--- title: Docker Compose Patterns description: Reusable patterns for structuring Docker Compose applications in homelab and development environments tags: - containers - docker - compose category: containers created: 2026-03-14 updated: 2026-03-14 --- # Docker Compose Patterns ## 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 ```yaml services: app: image: ghcr.io/example/app:1.2.3 ports: - "8080:8080" ``` Start it: ```bash docker compose up -d docker compose ps ``` ### 2. Add persistent storage and configuration ```yaml 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 ```yaml 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 `version` field 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 ## References - [Docker: Compose file reference](https://docs.docker.com/reference/compose-file/) - [Docker: Compose application model](https://docs.docker.com/compose/intro/compose-application-model/) - [Docker: Control startup and shutdown order in Compose](https://docs.docker.com/compose/how-tos/startup-order/) - [Compose Specification](https://compose-spec.io/)