Build automation: Make for repeatable workflows
Small teams often rely on scattered README commands. make gives a single, discoverable entrypoint for common tasks. It's lightweight, widely available, and works well across languages and tooling.
Why Make?​
- Visibility:
make helpshows all available targets at a glance. - Consistency: Single source of truth for common workflows.
- Simplicity: Lightweight—no complex task runners needed.
- Portability: Works across languages, package managers, and deployment tools.
Design Principles​
- Keep targets simple and composable; call scripts for complex logic.
- Use
.PHONYto declare targets that don't represent files. - Document targets via a
helptarget. - Targets should be idempotent—safe to run repeatedly during development.
Generic example​
Below is a basic Makefile structure for a simple project:
.PHONY: help build dev test lint fmt docker-build clean
help:
@echo "Available targets: build dev test lint fmt docker-build clean"
build:
@echo "Building project..."
# example: pnpm build or go build, etc.
pnpm build
dev:
@echo "Starting dev environment..."
pnpm dev
test:
pnpm test
lint:
pnpm lint
fmt:
pnpm run format
docker-build:
docker build -t myapp:latest .
clean:
rm -rf dist node_modules
QMS monorepo example​
The QMS workspace uses PNPM, Docker Compose, database migrations, and observability services. Below is a real example Makefile adapted from the QMS project:
QMS Makefile source​
.PHONY: up down up-test up-lgtm down-lgtm up-mssql down-mssql clean help
help:
@echo "QMS Make targets:"
@echo " make up - Start full stack (DB, API, Web, Adminer)"
@echo " make down - Stop and clean stack"
@echo " make up-test - Start test containers (API tests, E2E tests)"
@echo " make up-lgtm - Start observability (Loki, Grafana, Prometheus)"
@echo " make down-lgtm - Stop observability stack"
@echo " make up-mssql - Start MSSQL restore container"
@echo " make down-mssql - Stop MSSQL restore"
@echo " make clean - Clean all stacks and prune Docker"
# Full local stack: DB (with migrations), Adminer, API, Web
up:
docker-compose up -d --build qms-db --wait
docker-compose up --build qms-db-migrate
docker-compose up --build qms-db-migrate-test
docker-compose up -d --build qms-adminer
docker-compose up -d --build qms-api
docker-compose up -d --build qms-web
# Stop and remove all containers created by main compose file
down:
docker-compose down -v --remove-orphans
# Start test-specific services
up-test:
docker-compose up -d --build qms-api-test
docker-compose up -d --build qms-e2e-test
# Start observability stack (Loki, Grafana, Prometheus)
up-lgtm:
docker-compose -p qms-lgtm -f docker-compose.observability.yaml up -d --build lgtm qms-db-exporter prometheus
down-lgtm:
docker-compose -p qms-lgtm -f docker-compose.observability.yaml down -v --remove-orphans
# MSSQL restore for data import testing
up-mssql:
docker-compose -p qms-mssql-restore-db -f restore_db/docker-compose.yml up -d
down-mssql:
docker-compose -p qms-mssql-restore-db -f restore_db/docker-compose.yml down -v --remove-orphans
# Full cleanup: all stacks + Docker housekeeping
clean:
docker-compose down -v --remove-orphans
docker-compose -p qms-lgtm -f docker-compose.observability.yaml down -v --remove-orphans
docker-compose -p qms-mssql-restore-db -f restore_db/docker-compose.yml down -v --remove-orphans
docker system prune -f
docker network prune -f
Common QMS workflows​
Start development:
make up
Run tests:
make up-test
# Run your test commands (pnpm test, etc.)
Enable observability:
make up-lgtm
# Grafana available at http://localhost:3000
Clean everything:
make clean
Adding dev server targets​
To extend the QMS Makefile with package-scoped dev servers, add:
.PHONY: api-dev web-dev build test lint
# API dev server
api-dev:
pnpm --filter qms-api start:dev
# Web dev server
web-dev:
pnpm --filter qms-web dev
# Workspace builds/tests
build:
pnpm -w -r build
test:
pnpm -w -r test
lint:
pnpm -w -r lint
Then run:
make api-dev # Start API in watch mode
make web-dev # Start Web in dev mode
Best practices​
- One target = one responsibility. Use
upto orchestrate multiple services, not to do ten unrelated things. - Destructive + cleanup. Pair
up-Xwithdown-X, and provide acleantarget that removes volumes and prunes. - Use
--waitstrategically. Wait for critical services (database) before dependent services start. - Document via
help. Add a smallhelptarget so contributors can discover all workflows without reading the Makefile. - Call scripts, don't embed logic. If a target needs complex logic, move it to a shell script and call it from Make.
macOS setup​
Xcode Command Line Tools must be installed to use make:
xcode-select --install
If already installed, verify:
make --version