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.

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