KhueApps
Home/DevOps/How to use a Makefile to simplify Docker and Compose commands

How to use a Makefile to simplify Docker and Compose commands

Last updated: October 06, 2025

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)

  1. Choose image name and service
  • Edit APP_NAME and IMAGE in the Makefile.
  • Update docker-compose.yml service name (web) if needed.
  1. 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.
  1. 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.
  1. Parameterize ports and tags
  • In Makefile: PORT ?= 8000, TAG ?= dev.
  • Use: IMAGE := $(APP_NAME):$(TAG).
  • Invoke: make TAG=staging compose-up.
  1. 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.
  1. 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.

Series: Docker

DevOps