Docker Compose basics for self-hosting
Docker Compose is how the self-hosting world ships software: almost every
project's install instructions are "here's a compose.yaml, run
docker compose up -d". You don't need to understand containers deeply
to use it well — you need the small set of ideas below, which cover practically
every service you'll ever deploy.
Install
Use Docker's own repository, not your distro's often-stale package. Their convenience script is fine for a personal server:
curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER # log out and in again docker compose version
Note the space: docker compose (v2, a plugin) is current;
docker-compose with a hyphen is the old Python tool, and tutorials
that use it are showing their age.
Anatomy of a compose file
One directory per service, containing a compose.yaml. Here's a
realistic one — Vaultwarden, the password manager — annotated:
services:
vaultwarden:
image: vaultwarden/server:latest # which image, from Docker Hub
container_name: vaultwarden
restart: unless-stopped # come back after reboots & crashes
environment:
- DOMAIN=https://vault.example.com
- SIGNUPS_ALLOWED=false
volumes:
- ./data:/data # host path : path inside container
networks:
- proxy # reachable by the reverse proxy
networks:
proxy:
external: true
The lines that matter, in order of how often they bite people:
volumes— where the data lives. Without a volume, everything a container writes vanishes when it's recreated. Prefer bind mounts (./data:/data) for self-hosting: the files sit in a normal directory next to the compose file, trivially backed up and inspected. Named volumes (vw_data:/data) are fine too, just harder to find on disk.restart: unless-stopped— always set it. It's the difference between a server that survives a reboot and one that doesn't.ports— deliberately absent above. Publishing- "8080:80"exposes the service on the host's port 8080 to anything that can reach the host — including the internet, if the firewall allows it. Behind a reverse proxy you don't need it: the proxy reaches the container over the shared Docker network by name. Publish ports only for LAN-only setups without a proxy, and then bind them to a specific interface ("192.168.1.10:8080:80") or localhost.environment— the app's settings. Move secrets into a.envfile next to the compose file (KEY=valueper line, referenced as${KEY}) so the compose file itself is safe to share or version.- image tags —
:latestis convenient and occasionally surprising (a major-version jump on a Tuesday). For anything with a database, pinning a major version (postgres:16) and upgrading deliberately is the calmer life.
Multi-container stacks
Apps that need a database bring it along in the same file. The pattern is
always the same — a second service, a private link, credentials in
.env:
services:
app:
image: someapp:2
environment:
- DB_HOST=db
- DB_PASSWORD=${DB_PASSWORD}
depends_on:
- db
networks: [proxy, internal]
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- ./db:/var/lib/postgresql/data
networks: [internal] # database is NOT on the proxy network
networks:
proxy:
external: true
internal:
Containers in the same compose file resolve each other by service name
(DB_HOST=db just works). Keeping the database off the proxy network is
cheap defence in depth.
The commands you'll actually use
docker compose up -d # start (or apply changes) in the background docker compose ps # what's running, and is it healthy? docker compose logs -f app # follow one service's logs docker compose down # stop and remove containers (data in volumes survives) docker compose pull && docker compose up -d # update to newer images docker compose exec app sh # a shell inside a running container docker system prune # reclaim space from old images (run monthly)
That last update line is your monthly maintenance ritual — combine it with OS updates and a glance at the backup log. Automated updaters (Watchtower) exist but turn "surprising major version" from a possibility into a scheduled event; most experienced self-hosters update on purpose.
Where to put things
A convention that pays off: /opt/stacks/<service>/compose.yaml
with the data directories inside each. Your entire server is then one tree of
compose files plus data — back up /opt/stacks, and rebuilding onto a
fresh machine is rsync + up -d per directory. Many people
keep the compose files (not the data) in a private git repo as documentation of
their setup. That's the real payoff of Compose: your infrastructure is a set of
readable text files, not a memory of what you clicked.