How to Use Docker Compose (Run Multi-Container Apps from One File)

A docker run command with five -v flags, four -e variables and a --restart policy is unreadable and impossible to remember. Docker Compose puts all of that in a file. docker compose up starts it, down stops it, and when you want to change something you edit the file rather than retype a command. Most self-hosted apps publish a ready-made compose file — you just copy it.
Quick reference
| Task | Command (run in the folder with the compose file) |
|---|---|
| Start everything in the background | docker compose up -d |
| Stop and remove containers | docker compose down |
| See status | docker compose ps |
| Logs | docker compose logs -f |
| Update to the latest images | docker compose pull && docker compose up -d |
| Restart one service | docker compose restart web |
1. Your first compose file
Make a folder per app:
mkdir -p ~/docker/uptime-kuma && cd ~/docker/uptime-kuma
nano compose.yaml
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
ports:
- "3001:3001"
volumes:
- ./data:/app/data
restart: unless-stopped
Each line maps to a docker run flag: ports is -p, volumes is -v, restart is --restart. ./data is a folder next to the file, so the app's data sits in ~/docker/uptime-kuma/data where you can see and back it up.
Indentation is two spaces and matters — YAML is fussy. (Older guides name the file docker-compose.yml; both work.)
2. Start it
docker compose up -d
It pulls the image, creates the container and starts it. -d detaches so you get your prompt back. Open http://server-ip:3001.
3. Look after it
docker compose ps
docker compose logs -f
docker compose restart
docker compose down
down stops and deletes the containers but not the data in ./data. Run up -d again and it's back exactly as it was.
4. More than one container
The point of Compose is apps that need a database too. WordPress, for example:
services:
db:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: changeme
MARIADB_DATABASE: wordpress
MARIADB_USER: wp
MARIADB_PASSWORD: changeme
volumes:
- ./db:/var/lib/mysql
restart: unless-stopped
wordpress:
image: wordpress:6
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wp
WORDPRESS_DB_PASSWORD: changeme
WORDPRESS_DB_NAME: wordpress
volumes:
- ./html:/var/www/html
depends_on:
- db
restart: unless-stopped
Containers in the same file can reach each other by service name — WordPress finds the database at the hostname db. Nothing about the database is exposed to the network.
5. Keep passwords out of the file
Create a .env file next to compose.yaml:
DB_PASSWORD=a-long-random-string
and refer to it as ${DB_PASSWORD} in the compose file. Now the compose file can be shared or committed without leaking anything.
6. Updating an app
docker compose pull
docker compose up -d
It downloads newer images and recreates only the containers that changed. Then docker image prune to remove the old ones.
Organising a server
~/docker/
nextcloud/compose.yaml
pihole/compose.yaml
uptime-kuma/compose.yaml
One folder per app, data in ./data inside it, and backing up the whole server is just backing up ~/docker.
Troubleshooting
"yaml: line 7: did not find expected key." Indentation. Every level is two spaces, no tabs.
"docker compose" not found but "docker-compose" works. You have the old standalone version. The install script from tutorial 24 provides the new plugin; sudo apt install docker-compose-plugin adds it.
Container keeps restarting. docker compose logs service-name for the reason. Usually a wrong environment variable or a volume folder with the wrong owner.
Changed the file but nothing happened. You need docker compose up -d again — it notices the difference and recreates what changed.
Wrapping up
One folder, one compose.yaml, up -d to start, down to stop, pull then up -d to update. Copy the file from any app's docs, change the ports and passwords, and you're running it in a minute. This is how everything else in this series gets installed.
