Skip to content
Portfolio

Writing Docker Compose

Docker Compose defines and runs multi-container applications from a single YAML file. Instead of starting each container manually with docker run, Compose orchestrates services, networks, and volumes together.

For single-container workflows, see the Docker CLI Cheat Sheet. For building custom images, see Writing Dockerfiles. To run a local database for SQL practice, see Local database with Docker.

Create a compose.yaml (or docker-compose.yaml) in your project root:

services:
web:
image: nginx:alpine
ports:
- "8080:80"
restart: unless-stopped
api:
build: .
ports:
- "3000:3000"
environment:
APP_ENV: production
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:

Start the stack:

Terminal window
docker compose up -d
KeyDescription
servicesContainers that make up the application (required)
networksCustom networks for service communication
volumesNamed volumes for persistent data
configsExternal configuration files mounted into services
secretsSensitive data (passwords, tokens) managed separately
OptionDescription
imageUse a pre-built image from a registry
buildBuild from a Dockerfile (. or { context: ., dockerfile: Dockerfile.prod })
portsPublish ports ("host:container")
exposeExpose ports to other services only (not the host)
environmentEnvironment variables (map or list)
env_fileLoad variables from a .env file
volumesMount named volumes or bind mounts
networksAttach the service to one or more networks
depends_onStart order dependency (does not wait for the service to be ready)
restartRestart policy: no, always, on-failure, unless-stopped
commandOverride the default container command
entrypointOverride the default entrypoint
healthcheckDefine a health probe for the service
profilesOptional services activated with --profile

Build from the current directory:

services:
api:
build: .

Build with a custom Dockerfile and build arguments:

services:
api:
build:
context: .
dockerfile: Dockerfile.prod
args:
JAVA_VERSION: "21"

Named volume — managed by Docker, survives container restarts:

services:
db:
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:

Bind mount — maps a host path into the container (common in development):

services:
api:
volumes:
- ./src:/app/src

By default, all services join a default network and can reach each other by service name. Define custom networks to isolate groups:

services:
frontend:
networks:
- public
backend:
networks:
- public
- internal
db:
networks:
- internal
networks:
public:
internal:
internal: true

backend can talk to both frontend and db; db is only reachable from internal.

Inline map:

environment:
APP_ENV: production
LOG_LEVEL: info

From a file:

env_file:
- .env
- .env.production

Use a .env file in the same directory for local defaults (not committed if it contains secrets):

APP_ENV=development
POSTGRES_PASSWORD=localdev
CommandDescription
docker compose upCreate and start all services
docker compose up -dStart in detached mode (background)
docker compose downStop and remove containers, networks
docker compose down -vAlso remove named volumes
docker compose psList running services
docker compose logsView logs from all services
docker compose logs -f apiFollow logs for a single service
docker compose buildBuild or rebuild service images
docker compose pullPull images for all services
docker compose restart apiRestart a single service
docker compose exec api shRun a command inside a running container

Rebuild after Dockerfile changes:

Terminal window
docker compose up -d --build

5. Example: web app with cache and database

Section titled “5. Example: web app with cache and database”
services:
app:
build: .
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://app:secret@db:5432/appdb
REDIS_URL: redis://cache:6379
depends_on:
- db
- cache
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- db-data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
db-data:
redis-data:

Services resolve each other by name — app connects to db:5432 and cache:6379 over the default network.

Run optional services only when needed (e.g., debugging tools):

services:
api:
image: my-api:latest
debug:
image: nicolaka/netshoot
profiles:
- debug
network_mode: service:api
Terminal window
docker compose --profile debug up -d
  1. Use service names as hostnames — no need for hardcoded IPs.
  2. Store secrets in .env or Docker secrets, not in the compose file itself.
  3. Pin image tags (postgres:16-alpine, not postgres:latest).
  4. Use named volumes for database persistence.
  5. Add restart: unless-stopped for services that should survive host reboots.
  6. Use docker compose config to validate and render the final YAML before deploying.