01Introduction
A well-structured Docker Compose file is the foundation of any maintainable self-hosted setup. Poor organization leads to configuration drift, security vulnerabilities, and debugging nightmares. This guide covers battle-tested patterns for structuring your compose files.
02Basic File Structure
Every Docker Compose file should follow a consistent structure. Start with the version (optional in Compose V2+), then services, networks, and volumes. Group related configuration together and use comments to explain non-obvious choices.
1# Service: Nextcloud with PostgreSQL2# Purpose: Self-hosted cloud storage3# Last updated: 2024-0145services: 6 # Application tier7 nextcloud: 8 image: nextcloud:latest9 container_name: nextcloud10 restart: unless-stopped11 depends_on: 12 - db13 ports: 14 - "8080:80"15 environment: 16 - POSTGRES_HOST=db17 - POSTGRES_DB=${DB_NAME}18 - POSTGRES_USER=${DB_USER}19 - POSTGRES_PASSWORD=${DB_PASSWORD}20 volumes: 21 - nextcloud_data:/var/www/html22 networks: 23 - frontend24 - backend2526 # Database tier27 db: 28 image: postgres:15-alpine29 container_name: nextcloud-db30 restart: unless-stopped31 environment: 32 - POSTGRES_DB=${DB_NAME}33 - POSTGRES_USER=${DB_USER}34 - POSTGRES_PASSWORD=${DB_PASSWORD}35 volumes: 36 - db_data:/var/lib/postgresql/data37 networks: 38 - backend3940networks: 41 frontend: 42 name: nextcloud-frontend43 backend: 44 name: nextcloud-backend45 internal: true4647volumes: 48 nextcloud_data: 49 db_data: 03Naming Conventions
Consistent naming prevents confusion as your stack grows. Use lowercase with hyphens for service names. Prefix container names with the stack name. Use descriptive volume and network names that indicate their purpose.
Adopt a naming pattern like: {stack}-{service} for containers (e.g., nextcloud-db) and {stack}_{purpose} for volumes (e.g., nextcloud_data).
04File Organization
For complex stacks, split configuration across multiple files. Use docker-compose.override.yml for local development settings. Keep environment-specific configs in separate files and use the -f flag to compose them.
1# Directory structure for a production setup2my-stack/3├── docker-compose.yml # Base configuration4├── docker-compose.override.yml # Local dev overrides (auto-loaded)5├── docker-compose.prod.yml # Production-specific settings6├── .env # Environment variables7├── .env.example # Template for .env8├── config/ # Configuration files9│ ├── nginx.conf10│ └── app.conf11└── scripts/12 ├── backup.sh13 └── restore.sh1415# Running in production16docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d05Service Dependencies
Use depends_on to control startup order. For production, add healthchecks to ensure services are actually ready, not just started. The condition option lets you wait for healthy status.
1services: 2 app: 3 image: myapp:latest4 depends_on: 5 db: 6 condition: service_healthy7 redis: 8 condition: service_started910 db: 11 image: postgres:15-alpine12 healthcheck: 13 test: ["CMD-SHELL", "pg_isready -U postgres"]14 interval: 10s15 timeout: 5s16 retries: 51718 redis: 19 image: redis:alpine20 healthcheck: 21 test: ["CMD", "redis-cli", "ping"]22 interval: 10s23 timeout: 5s24 retries: 506Best Practices Summary
Following these practices will save you hours of debugging and make your stacks easier to maintain and share with others.
Always pin image versions in production (e.g., postgres:15.4-alpine instead of postgres:latest). Use latest only for development.
Never commit .env files to version control. Always add them to .gitignore and provide a .env.example template instead.