docker.recipes
Fundamentals8 min read

How to Structure Docker Compose Files for Self-Hosting

Learn the best practices for organizing and structuring your Docker Compose files for maintainable self-hosted applications.

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 PostgreSQL
2# Purpose: Self-hosted cloud storage
3# Last updated: 2024-01
4
5services:
6 # Application tier
7 nextcloud:
8 image: nextcloud:latest
9 container_name: nextcloud
10 restart: unless-stopped
11 depends_on:
12 - db
13 ports:
14 - "8080:80"
15 environment:
16 - POSTGRES_HOST=db
17 - POSTGRES_DB=${DB_NAME}
18 - POSTGRES_USER=${DB_USER}
19 - POSTGRES_PASSWORD=${DB_PASSWORD}
20 volumes:
21 - nextcloud_data:/var/www/html
22 networks:
23 - frontend
24 - backend
25
26 # Database tier
27 db:
28 image: postgres:15-alpine
29 container_name: nextcloud-db
30 restart: unless-stopped
31 environment:
32 - POSTGRES_DB=${DB_NAME}
33 - POSTGRES_USER=${DB_USER}
34 - POSTGRES_PASSWORD=${DB_PASSWORD}
35 volumes:
36 - db_data:/var/lib/postgresql/data
37 networks:
38 - backend
39
40networks:
41 frontend:
42 name: nextcloud-frontend
43 backend:
44 name: nextcloud-backend
45 internal: true
46
47volumes:
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 setup
2my-stack/
3├── docker-compose.yml # Base configuration
4├── docker-compose.override.yml # Local dev overrides (auto-loaded)
5├── docker-compose.prod.yml # Production-specific settings
6├── .env # Environment variables
7├── .env.example # Template for .env
8├── config/ # Configuration files
9│ ├── nginx.conf
10│ └── app.conf
11└── scripts/
12 ├── backup.sh
13 └── restore.sh
14
15# Running in production
16docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

05Service 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:latest
4 depends_on:
5 db:
6 condition: service_healthy
7 redis:
8 condition: service_started
9
10 db:
11 image: postgres:15-alpine
12 healthcheck:
13 test: ["CMD-SHELL", "pg_isready -U postgres"]
14 interval: 10s
15 timeout: 5s
16 retries: 5
17
18 redis:
19 image: redis:alpine
20 healthcheck:
21 test: ["CMD", "redis-cli", "ping"]
22 interval: 10s
23 timeout: 5s
24 retries: 5

06Best 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.