Makefiles are ancient. They’re also incredibly useful for modern development. Here’s how to use them as your project’s command center.
Why Make in 2026?# Every project has commands you run repeatedly:
Start development servers Run tests Build containers Deploy to environments Format and lint code You could remember them all. Or document them in a README that gets stale. Or put them in a Makefile where they’re executable documentation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.PHONY : dev test build deploy
dev :
docker-compose up -d
npm run dev
test :
pytest -v
build :
docker build -t myapp:latest .
deploy :
kubectl apply -f k8s/
Now make dev starts everything. New team member? Run make help.
The Self-Documenting Makefile# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.DEFAULT_GOAL := help
.PHONY : help
help : ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $( MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
.PHONY : dev
dev : ## Start development environment
docker-compose up -d
.PHONY : test
test : ## Run test suite
pytest -v --cov= src
.PHONY : lint
lint : ## Run linters
ruff check src/
mypy src/
.PHONY : build
build : ## Build production container
docker build -t myapp:$$ ( git rev-parse --short HEAD) .
Running make with no arguments now prints:
d t l b e e i u v s n i t t l d S R R B t u u u a n n i r l t t l d e i d s n p e t t r v e o e s r d l u s u o i c p t t m e i e o n n t c e o n n v t i a r i o n n e m r e n t
Variables and Defaults# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Defaults that can be overridden
IMAGE_NAME ?= myapp
IMAGE_TAG ?= $( shell git rev-parse --short HEAD)
REGISTRY ?= ghcr.io/myorg
# Derived values
FULL_IMAGE = $( REGISTRY) /$( IMAGE_NAME) :$( IMAGE_TAG)
.PHONY : build
build : ## Build container image
docker build -t $( FULL_IMAGE) .
.PHONY : push
push : build ## Build and push to registry
docker push $( FULL_IMAGE)
Override at runtime:
1
2
make build IMAGE_TAG = v1.2.3
make push REGISTRY = docker.io/myuser
Environment-Aware Targets# 1
2
3
4
5
6
7
8
9
10
11
12
13
ENV ?= development
.PHONY : deploy
deploy : ## Deploy to environment (ENV=development|staging|production)
ifeq ( $( ENV ) ,production)
@echo "Deploying to PRODUCTION - are you sure?"
@read -p "Type 'yes' to continue : " confirm && [ "$$confirm " = "yes " ]
endif
kubectl apply -k k8s/overlays/ $( ENV ) /
.PHONY : logs
logs : ## Tail logs for environment
kubectl logs -f -l app = myapp --namespace= $( ENV)
Dependency Chains# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Build artifacts with dependencies
dist/app.js : src /*.ts package .json
npm run build
# Only rebuild when sources change
.PHONY : build
build : dist /app .js
# Clean removes artifacts
.PHONY : clean
clean :
rm -rf dist/ node_modules/ .pytest_cache/
# Fresh build
.PHONY : rebuild
rebuild : clean build
Grouped Targets# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Composite targets for common workflows
.PHONY : ci
ci : lint test build ## Run full CI pipeline
.PHONY : setup
setup : ## Initial project setup
python -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -r requirements-dev.txt
cp .env.example .env
@echo "Setup complete. Run 'source .venv/bin/activate'"
.PHONY : reset
reset : clean setup ## Clean slate reset
Include Files for Organization# 1
2
3
4
5
6
# Makefile
include make/docker.mk
include make/test.mk
include make/deploy.mk
.DEFAULT_GOAL := help
1
2
3
4
5
6
7
8
9
10
11
# make/docker.mk
.PHONY : docker -build docker -push docker -run
docker-build :
docker build -t $( IMAGE) .
docker-push : docker -build
docker push $( IMAGE)
docker-run :
docker run -it --rm -p 8080:8080 $( IMAGE)
Shell Commands and Scripting# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Multi-line commands (note the semicolons and backslashes)
.PHONY : check -tools
check-tools :
@command -v docker >/dev/null 2>& 1 || { echo "docker required" ; exit 1; }
@command -v kubectl >/dev/null 2>& 1 || { echo "kubectl required" ; exit 1; }
@echo "All tools available"
# Capture command output
VERSION := $( shell cat VERSION)
GIT_SHA := $( shell git rev-parse --short HEAD)
BUILD_TIME := $( shell date -u +%Y-%m-%dT%H:%M:%SZ)
.PHONY : version
version :
@echo "Version: $( VERSION) "
@echo "Git SHA: $( GIT_SHA) "
@echo "Built: $( BUILD_TIME) "
Error Handling# 1
2
3
4
5
6
7
8
9
10
11
12
13
.PHONY : safe -deploy
safe-deploy :
# Stop on first error (default behavior)
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/deployment.yaml
kubectl rollout status deployment/myapp
.PHONY : best -effort
best-effort :
# Continue despite errors (- prefix)
-kubectl delete configmap old-config
-kubectl delete secret old-secret
kubectl apply -f k8s/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
UNAME := $( shell uname)
ifeq ( $( UNAME ) ,Darwin)
SED := gsed
OPEN := open
else
SED := sed
OPEN := xdg-open
endif
.PHONY : docs
docs :
mkdocs build
$( OPEN) site/index.html
Parallel Execution# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Run independent targets in parallel
.PHONY : ci -parallel
ci-parallel :
$( MAKE) -j3 lint test build
# Or mark targets as parallelizable
.PHONY : lint -all
lint-all : lint -python lint -js lint -yaml
lint-python :
ruff check .
lint-js :
eslint src/
lint-yaml :
yamllint .
Run with: make -j4 lint-all
Common Patterns# Database Tasks# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.PHONY : db -migrate db -rollback db -seed db -reset
db-migrate :
alembic upgrade head
db-rollback :
alembic downgrade -1
db-seed :
python scripts/seed_data.py
db-reset : ## Reset database (DANGEROUS)
@read -p "This will DELETE all data. Continue? [y/N] " confirm && [ " $$ confirm" = "y" ]
alembic downgrade base
alembic upgrade head
$( MAKE) db-seed
Secret Management# 1
2
3
4
5
6
7
8
.PHONY : secrets -encrypt secrets -decrypt
secrets-encrypt :
sops --encrypt --in-place secrets/$( ENV) .yaml
secrets-decrypt :
sops --decrypt secrets/$( ENV) .yaml > .secrets.yaml
@echo "Decrypted to .secrets.yaml (git-ignored)"
Local vs CI Detection# 1
2
3
4
5
6
7
ifdef CI
DOCKER_OPTS := --quiet
TEST_OPTS := --no-header
else
DOCKER_OPTS :=
TEST_OPTS := -v
endif
The Principles# Make is for humans : Optimize for discoverabilitySelf-documenting : make help should explain everythingComposable : Small targets that chain togetherOverride-friendly : Sensible defaults, easy to customizeCross-platform aware : Handle macOS/Linux differencesThe best automation is the kind your team actually uses. Make makes it easy.
📬 Get the Newsletter Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.