Docker Compose Patterns
Reusable patterns for structuring Docker Compose applications in homelab and development environments
Introduction
Docker Compose defines multi-container applications in a single declarative file. It is a good fit for homelab stacks, local development, and small self-hosted services that do not require a full orchestrator.
Purpose
Compose helps when you need:
- Repeatable service definitions
- Shared networks and volumes for a stack
- Environment-specific overrides
- A clear deployment artifact that can live in Git
Architecture Overview
A Compose application usually includes:
- One or more services
- One or more shared networks
- Persistent volumes
- Environment variables and mounted configuration
- Optional health checks and startup dependencies
Step-by-Step Guide
1. Start with a minimal Compose file
services:
app:
image: ghcr.io/example/app:1.2.3
ports:
- "8080:8080"Start it:
docker compose up -d
docker compose ps2. Add persistent storage and configuration
services:
app:
image: ghcr.io/example/app:1.2.3
ports:
- "8080:8080"
environment:
APP_BASE_URL: "https://app.example.com"
volumes:
- app-data:/var/lib/app
volumes:
app-data:3. Add dependencies with health checks
services:
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- db-data:/var/lib/postgresql/data
app:
image: ghcr.io/example/app:1.2.3
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
ports:
- "8080:8080"
volumes:
db-data:Common Patterns
Use one project directory per stack
Keep the Compose file, .env example, and mounted config together in one directory.
Use user-defined networks
Private internal services should communicate over Compose networks rather than the host network.
Prefer explicit volumes
Named volumes are easier to back up and document than anonymous ones.
Use profiles for optional services
Profiles are useful for dev-only services, one-shot migration jobs, or optional observability components.
Troubleshooting Tips
Services start in the wrong order
- Use health checks instead of only container start order
- Ensure the application retries database or dependency connections
Configuration drift between hosts
- Commit the Compose file to Git
- Keep secrets out of the file and inject them separately
- Avoid host-specific bind mount paths when portability matters
Containers cannot resolve each other
- Check that the services share the same Compose network
- Use the service name as the hostname
- Verify the application is not hard-coded to
localhost
Best Practices
- Omit the deprecated top-level
versionfield in new Compose files - Keep secrets outside the Compose YAML when possible
- Pin images to intentional versions
- Use health checks for stateful dependencies
- Treat Compose as deployment code and review changes like application code