Use Docker Compose for Multi-Service Apps
Define your entire application stack in one file — media servers, databases, web apps — and start everything with a single command.
What is Docker Compose?
Docker Compose is a tool that lets you define and run multiple Docker containers as a single coordinated application. Instead of typing long docker run commands for each service, you write one YAML file and run docker compose up.
- •A container is a lightweight, isolated package that runs an application — like a virtual machine but much faster and more efficient.
- •A compose file (docker-compose.yml) describes all your containers, how they connect, and where they store data.
- •One command — docker compose up — starts every service in the right order with the correct configuration.
- •Perfect for stacks that need multiple parts working together: a media server (Jellyfin) + TV show tracker (Sonarr) + movie tracker (Radarr), all connected through a shared network.
Step 1. Anatomy of a Compose File
A compose file has three core sections. Let's walk through a real example line by line:
- •services — the containers you want to run. Each service gets a name, an image, ports, and environment variables.
- •volumes — persistent storage that survives when containers restart. Without volumes, all your data disappears when a container stops.
- •networks — how containers talk to each other. By default, all services in the same compose file can reach each other by service name.
- •environment — settings passed into the container, like passwords, time zones, and configuration paths.
- •ports — maps a port on your host machine (left side) to a port inside the container (right side). "8080:80" means your server's port 8080 reaches the container's port 80.
services:
web:
image: nginx:latest # The container image to use
ports:
- "8080:80" # Host port 8080 → container port 80
volumes:
- ./website:/usr/share/nginx/html # Local folder → container folder
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: supersecurepassword
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: # Named volume — Docker manages where it lives on disk
Step 2. Your First Compose Stack — Web App + Database
Start with something simple to understand the workflow. Create a two-service stack that runs a web server and a database:
- •Create a new folder for your project and save the compose file above as docker-compose.yml inside it.
- •Run docker compose up -d from that folder. The -d flag means "detached" — it runs in the background.
- •Open your browser and go to http://localhost:8080 — you should see the default NGINX welcome page.
- •Run docker compose down to stop everything. Add -v (docker compose down -v) to also delete the volumes — useful when you're experimenting and want a clean slate.
- •Your website files live in ./website on your computer. Edit them and refresh the browser — changes are instant because the folder is mounted into the container.
# Start everything in the background
docker compose up -d
# Check what's running
docker compose ps
# Stop and remove everything
docker compose down
Step 3. Real-World Example — A Complete Media Stack
Here's a practical compose file for a home media server. Jellyfin streams your videos, Sonarr tracks TV shows, and Radarr tracks movies — all connected through a shared network:
- •Shared network — all services connect via the "media" network so they can communicate by name (e.g., Sonarr reaches Jellyfin at http://jellyfin:8096).
- •Persistent volumes — each service stores its configuration and data in named volumes. Your Jellyfin library survives container restarts and updates.
- •Media folder mount — all services mount /path/to/your/media so they share the same video files. Sonarr renames episodes into folders that Jellyfin automatically detects.
services:
jellyfin:
image: jellyfin/jellyfin:latest
ports: ["8096:8096"]
volumes:
- jellyfin-config:/config
- /mnt/media:/media:ro # read-only access to media files
networks: [media]
sonarr:
image: linuxserver/sonarr:latest
ports: ["8989:8989"]
volumes:
- sonarr-config:/config
- /mnt/media/tv:/tv
networks: [media]
radarr:
image: linuxserver/radarr:latest
ports: ["7878:7878"]
volumes:
- radarr-config:/config
- /mnt/media/movies:/movies
networks: [media]
volumes:
jellyfin-config:
sonarr-config:
radarr-config:
networks:
media:
Step 4. Managing Your Stack Day to Day
Once your stack is running, these commands cover 90% of day-to-day operations:
- •docker compose ps — see all running containers at a glance. Shows status, ports, and how long they've been up.
- •docker compose logs -f jellyfin — watch live logs from a specific service. Replace "jellyfin" with any service name. Use Ctrl+C to exit.
- •docker compose restart sonarr — restart one service without touching the others. Useful after a config change.
- •docker compose pull — download the latest versions of all container images. Follow up with docker compose up -d to recreate containers with the new images.
- •docker compose down — stop and remove everything. Your volumes (data) are preserved. Add -v to delete volumes too when you want a full clean-up.
- •docker compose exec jellyfin bash — open a shell inside a running container. Useful for debugging (checking config files, testing network connectivity).
# See everything running
docker compose ps
# Watch logs for one service
docker compose logs -f jellyfin
# Update all images and restart
docker compose pull && docker compose up -d
# Drop into a container's shell
docker compose exec jellyfin bash
Step 5. Best Practices for Production
A few habits make the difference between a fragile hobby setup and something you can rely on for years:
- •Use .env files for secrets — never put passwords directly in docker-compose.yml. Create a .env file in the same folder and reference variables with ${VARIABLE_NAME}. Your compose file stays clean and shareable.
- •Pin your image versions — use "postgres:16" or even "postgres:16.3" instead of "postgres:latest". The :latest tag can introduce breaking changes when you pull updates. Pinned versions give you control over when you upgrade.
- •Set restart policies — add "restart: unless-stopped" to every service. If your server reboots, all containers come back up automatically. If you manually stop one, it stays stopped.
- •Use healthchecks — tell Docker how to check if a service is actually working, not just running. A web service might have "healthcheck: test: ["CMD", "curl", "-f", "http://localhost:80"]". Docker can auto-restart unhealthy containers.
- •Keep it in version control — save your docker-compose.yml in a Git repository. It's your infrastructure-as-code. If your server dies, you can rebuild everything from that one file.
# .env file — keep this out of git!
DB_PASSWORD=supersecurepassword
JELLYFIN_API_KEY=abc123...
# In docker-compose.yml, reference it:
# environment:
# POSTGRES_PASSWORD: ${DB_PASSWORD}
Quick Tips
- •Use docker compose up -d --remove-orphans when you rename or remove services — it cleans up containers no longer in your compose file.
- •Put your compose files in separate folders (e.g., ~/docker/media-stack/ and ~/docker/monitoring/). Each folder is its own isolated stack.
- •Use docker compose config to validate your compose file and see the fully resolved configuration (with .env variables expanded).
- •If a container can't reach another by name, check they're on the same network. Run docker compose exec service-name ping other-service to test connectivity.
Continue Reading
Ready to Orchestrate Your Stack?
Docker Compose turns a pile of containers into a single, manageable application. Start with two services and grow from there.
Browse All How-To Guides