Skip to main content

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 help shows 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 .PHONY to declare targets that don't represent files.
  • Document targets via a help target.
  • 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​

  1. One target = one responsibility. Use up to orchestrate multiple services, not to do ten unrelated things.
  2. Destructive + cleanup. Pair up-X with down-X, and provide a clean target that removes volumes and prunes.
  3. Use --wait strategically. Wait for critical services (database) before dependent services start.
  4. Document via help. Add a small help target so contributors can discover all workflows without reading the Makefile.
  5. 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

See also​