Docker Compose is the tool that runs almost every self-hosted app: you describe the app's containers, storage and networking in one compose.yaml file, and docker compose up -d starts it. For self-hosters it replaces long docker run commands with a readable file you can version, back up and move to another machine. Learn six concepts (services, .env files, volumes, networks, restart policies and updates) and you can deploy nearly anything in our catalog. Each example below links to a full deploy guide that uses the same pattern.
Install Docker Compose
Compose today is a plugin for the Docker CLI, used as docker compose (with a space). The older standalone docker-compose binary (with a hyphen) is legacy.
- Linux: install Docker Engine from Docker's official repository for your distribution and include the
docker-compose-pluginpackage. Your distribution's owndocker.iopackage is often older. - Windows and macOS: Docker Desktop includes Compose.
Confirm it works:
docker compose version
Add your user to the docker group if you want to run commands without sudo, and remember that membership in that group is effectively root access. Our self-hosting roadmap covers preparing a fresh server.
Your first compose.yaml
Create one folder per app. Everything the app needs lives inside it, which makes backups and migrations simple.
services:
uptime-kuma:
image: louislam/uptime-kuma:2
container_name: uptime-kuma
restart: unless-stopped
ports:
- "127.0.0.1:3001:3001"
volumes:
- ./data:/app/data
Run docker compose up -d in that folder. The parts:
serviceslists containers. The key (uptime-kuma) is the service name, which also becomes its hostname on the project's network.imageis what to run. Pin at least the major version (:2) instead oflatest, so a breaking release never arrives by surprise.portsmaps host port to container port. Prefixing127.0.0.1:keeps the app reachable only from the server itself, ready for a reverse proxy.volumeskeeps data outside the container, so you can delete and recreate the container without losing anything.
This is the same setup our Uptime Kuma deploy guide builds on.
A note on file names: Compose accepts compose.yaml (preferred) and docker-compose.yml. You may still see a version: "3.8" line at the top of older examples; it is obsolete and can be deleted.
The .env file: settings and secrets
Compose automatically reads a file named .env in the project folder and substitutes its values into compose.yaml wherever you write ${NAME}. This is how most projects separate configuration from the file itself.
# .env
APP_VERSION=v3
UPLOAD_LOCATION=/srv/photos
DB_PASSWORD=change-me-to-a-long-random-string
services:
server:
image: ghcr.io/example/server:${APP_VERSION:-latest}
volumes:
- ${UPLOAD_LOCATION}:/data
environment:
DB_PASSWORD: ${DB_PASSWORD}
Two related features are easy to confuse:
| Feature | Read by | Purpose |
|---|---|---|
.env file in the project folder |
Compose | Fills in ${VARIABLES} in compose.yaml |
env_file: key on a service |
The container | Loads every line of a file into the container's environment |
environment: key |
The container | Sets individual variables inline |
Run docker compose config to print the final file with every variable resolved; it is the fastest way to debug a typo. Immich ships its official setup as a Compose file plus an .env file for the upload location, database password and version, so the Immich deploy guide is a good real-world example.
Treat .env as a secret. Restrict it with chmod 600 .env, never commit it to Git, and generate passwords with openssl rand -base64 32.
Volumes: where your data lives
Containers are disposable; volumes are not. Compose offers two kinds:
- Bind mounts (
./data:/app/data) map a host folder into the container. You can see and back up the files directly. Most self-hosters prefer them. - Named volumes (
db_data:/var/lib/postgresql/dataplus a top-levelvolumes:entry) are managed by Docker under/var/lib/docker/volumes. They avoid some permission issues and are common for databases.
Add :ro to mount something read-only, for example a media library that the app should never modify. Whichever you choose, the rule is the same: anything not in a volume is lost when the container is recreated.
Networks: how containers talk
Every Compose project gets its own default network, and services reach each other by service name. A web app connects to its database at db:5432, not at an IP address, and the database needs no published port at all.
services:
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
depends_on:
db:
condition: service_healthy
environment:
PAPERLESS_DBHOST: db
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U paperless"]
interval: 10s
retries: 5
depends_on with condition: service_healthy makes the app wait until the database actually accepts connections, which avoids a common first-start crash. Paperless-ngx runs this way with Postgres and a Redis-compatible broker; see the Paperless-ngx guide, and the Gitea guide for a Git server with Postgres.
Sharing a reverse proxy across projects. Create one network once (docker network create proxy), declare it as external: true in each project, and attach both the proxy and each web app to it. Traefik then discovers containers from their labels; the Traefik guide walks through it, and the Caddy guide shows the simpler file-based approach. If you are choosing, read Caddy vs Traefik.
A firewall warning. Ports you publish with Docker are opened through Docker's own iptables rules, which bypass UFW on Ubuntu. Publish only what must be public (normally just the proxy on 80 and 443) and bind everything else to 127.0.0.1.
Restart policies
| Policy | Behaviour | Use it for |
|---|---|---|
no (default) |
Never restarts | One-off tasks |
always |
Restarts on exit and on boot, even after you stopped it manually | Rarely what you want |
unless-stopped |
Restarts on exit and on boot, unless you stopped it | Almost every self-hosted service |
on-failure |
Restarts only after a non-zero exit code | Jobs and workers |
Everyday commands
docker compose up -d # create or update and start in the background
docker compose ps # status of this project's containers
docker compose logs -f app # follow one service's logs
docker compose exec app sh # open a shell inside a running container
docker compose down # stop and remove containers (volumes stay)
docker compose down -v also deletes named volumes. Do not type it on a project whose data you want to keep.
Updating safely
- Read the release notes for breaking changes, especially major versions.
- Back up first (see below).
- Run
docker compose pull, thendocker compose up -d. Compose recreates only containers whose image changed. - Clean old images occasionally with
docker image prune.
Fully automatic updaters are convenient, but for apps that hold data, a notification that an update exists is safer than an unattended upgrade.
Backing up a Compose project
Because each app lives in one folder, backups are straightforward:
- Files: back up the project folder:
compose.yaml,.envand every bind-mounted data folder. - Databases: do not copy a live database's files. Dump it instead, for example
docker compose exec -T db pg_dump -U app app > backup.sql, or stop the stack before copying. - Off-site: send it all to another location with an encrypted, deduplicating tool such as Restic or Kopia, and test a restore regularly.
Prefer a web UI?
Dockge manages Compose stacks from a browser while keeping the files on disk, and Portainer adds broader container management. Our Portainer vs Dockge comparison helps you choose.
Dockge
Fancy, easy-to-use manager for Docker compose stacks from the creator of Uptime Kuma.
Portainer
Web UI for managing Docker, Swarm and Kubernetes environments.
Every deploy guide on appsgit uses the patterns above. Pick an app from the 25 best self-hosted apps in 2026, make a folder, and write your next compose.yaml.