Docker Compose files can get messy fast if you don't have a consistent approach. Here's the structure I've settled on after running enough stacks to know what causes problems.
A real example — the media stack
services:
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
volumes:
- ./radarr/config:/config
- /mnt/media/movies:/movies
- /mnt/downloads:/downloads
ports:
- "7878:7878"
restart: unless-stopped
networks:
- media
networks:
media:
driver: bridge
The things I always include
container_name — always set this explicitly. Without it Docker generates a random name and docker logs becomes annoying. With it you can always just type docker logs radarr.
restart: unless-stopped — means the container restarts automatically if it crashes or the server reboots, but stays stopped if you manually stop it. Almost always what you want.
Named networks — putting related containers on the same named network means they can talk to each other by container name, not IP address. http://radarr:7878 from another container on the same network just works.
Environment variables
For anything sensitive — API keys, passwords, database credentials — I use a .env file alongside the Compose file and reference variables with ${VARIABLE_NAME}. The .env file goes in .gitignore. Never hardcode secrets in a Compose file that's going in version control.
One Compose file per stack
I keep related services together in one Compose file. The media stack is one file. The monitoring stack is another. The panel stack is another. This keeps things manageable — you can bring a whole stack up or down with one command, and the files stay readable.
Health checks
For services that other services depend on, I add health checks so Docker waits for them to be actually ready before starting dependent containers:
healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"] interval: 10s timeout: 5s retries: 5