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

Docker Compose: apps as a file — docker run flags you can actually keep.

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

TaskCommand (run in the folder with the compose file)
Start everything in the backgrounddocker compose up -d
Stop and remove containersdocker compose down
See statusdocker compose ps
Logsdocker compose logs -f
Update to the latest imagesdocker compose pull && docker compose up -d
Restart one servicedocker 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.

Call us