Why use a Makefile with Docker/Compose
Typing repetitive docker and docker compose commands is error-prone. A Makefile gives you short, memorable targets (build, up, logs, clean) and a single entry point for your team. This guide shows a minimal, practical setup and patterns you can adapt.
Minimal working example
Files:
- app.py
- Dockerfile
- docker-compose.yml
- Makefile
Create the following files in an empty directory.
# app.py
import http.server
import socketserver
import os
PORT = int(os.environ.get("PORT", 8000))
Handler = http.server.SimpleHTTPRequestHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"Serving on :{PORT}")
httpd.serve_forever()
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]
# docker-compose.yml
services:
web:
build: .
ports:
- "8000:8000"
environment:
- PORT=8000
profiles: [default]
# Makefile
SHELL := /bin/sh
APP_NAME := myapp
IMAGE := $(APP_NAME):dev
.DEFAULT_GOAL := help
.PHONY: help build run stop compose-up compose-down logs ps clean image-clean prune
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## ' Makefile | sed 's/:.*##/: /' | sort
build: ## Build the Docker image
docker build -t $(IMAGE) .
run: build ## Run the image locally (foreground)
docker run --rm -p 8000:8000 --name $(APP_NAME) $(IMAGE)
stop: ## Stop the running container (if exists)
-@docker stop $(APP_NAME) 2>/dev/null || true
compose-up: ## Start with Docker Compose (build + detached)
docker compose up -d --build
compose-down: ## Stop and remove Compose services and networks
docker compose down -v
logs: ## Follow logs for the web service
docker compose logs -f web
ps: ## List running Compose services
docker compose ps
image-clean: ## Remove built image
-@docker image rm -f $(IMAGE) 2>/dev/null || true
prune: ## Prune dangling images and caches (CAUTION)
docker system prune -f
clean: compose-down image-clean ## Full cleanup (down + image)
@true
Quickstart
- make help — list targets
- make build — build the image
- make run — run the container directly
- make compose-up — start via Docker Compose
- make logs — tail service logs
- make compose-down — stop services
- make clean — tear down and remove image
Try it:
make compose-up
curl -s http://localhost:8000 | head
make logs
make compose-down
Adapting to your project (numbered steps)
- Choose image name and service
- Edit APP_NAME and IMAGE in the Makefile.
- Update docker-compose.yml service name (web) if needed.
- Add build arguments or environment
- For Docker build-time args:
- Dockerfile: ARG APP_ENV
- make: docker build --build-arg APP_ENV=dev -t $(IMAGE) .
- For runtime env in Compose, add environment: entries or env_file.
- Use profiles for optional services
- In docker-compose.yml, add profiles: [dev] to services you want to start only in dev.
- Run: docker compose --profile dev up -d.
- Parameterize ports and tags
- In Makefile: PORT ?= 8000, TAG ?= dev.
- Use: IMAGE := $(APP_NAME):$(TAG).
- Invoke: make TAG=staging compose-up.
- Add standard targets per workflow
- test: run unit tests in container.
- fmt/lint: run tooling inside containers to avoid host deps.
- deploy: shell out to your CD script or compose -f overrides.
- Document with help
- Keep the "##" comments on targets so help stays accurate.
Common targets (what they do)
- build: docker build for a local dev tag.
- run: docker run for quick manual checks.
- compose-up: docker compose up -d --build for multi-service.
- logs: stream logs of a specific service (web by default).
- compose-down: cleanly stop and remove resources.
- clean: compose-down plus image removal.
- prune: free disk space by removing dangling resources.
Pitfalls and how to avoid them
- Tabs vs spaces: Make requires a literal tab before recipe lines. Using spaces will break the build.
- Compose command name: Newer setups use docker compose (space). Older ones use docker-compose (hyphen). Standardize one in your Makefile.
- Context bloat: docker build . sends the entire directory as build context. Use .dockerignore to exclude large files.
- Stale containers: If run leaves a container behind, make stop or use --rm to auto-remove.
- Variable expansion: Make variables (e.g., $(VAR)) differ from shell variables ($VAR). Quote and escape carefully.
- Cross-platform make: Some macOS systems ship BSD make. Prefer GNU make for advanced features or keep recipes POSIX-sh.
- Env propagation: Variables defined in Make don’t automatically flow into Dockerfile unless passed via --build-arg, and into containers via environment or --env.
- Ports in use: make run may fail if 8000 is busy. Parameterize PORT and override on the command line.
Performance notes
- Enable BuildKit: export DOCKER_BUILDKIT=1 for faster builds and better caching.
- Layer caching: Reorder Dockerfile so rarely-changed steps (apt-get, pip install) precede COPY of app code.
- Targeted builds: Use multi-stage builds and specify --target to avoid building heavy prod stages during dev.
- Build once, run many: Prefer compose-up --build once, then iterate with code volumes for faster cycles.
- Prune selectively: Use docker builder prune for build cache only instead of full system prune to avoid losing images you still need.
- Parallel targets: Keep independent targets separate; avoid .ONESHELL unless needed to retain caching and simpler debugging.
Extending the Makefile (snippets)
- Parameterized ports and tags:
PORT ?= 8000
TAG ?= dev
IMAGE := $(APP_NAME):$(TAG)
run:
docker run --rm -p $(PORT):$(PORT) --env PORT=$(PORT) --name $(APP_NAME) $(IMAGE)
- Compose profile:
PROFILE ?= default
compose-up:
docker compose --profile $(PROFILE) up -d --build
- Test inside container:
test: build
docker run --rm $(IMAGE) sh -lc "python -m unittest -q"
Tiny FAQ
Q: docker compose vs docker-compose? A: Prefer docker compose (V2). If your CI uses docker-compose, adjust the Makefile or set a variable alias.
Q: How do I pass environment variables? A: For builds, use --build-arg and ARG in Dockerfile. For runtime, use Compose environment: or env_file.
Q: How do I run a single service? A: docker compose up -d --build web or add a make target that narrows to that service.
Q: Can Make rebuild only when files change? A: You can declare file targets and use timestamps, but Docker’s cache usually suffices; keep simple phony targets for clarity.
Q: How do I see what make will run? A: Use make -n <target> to print commands without executing.