Skip to content
appsgit

Guides

Docker Compose for self-hosters: the only tutorial you need

Docker Compose for self-hosters: install it, then learn compose.yaml, .env files, volumes, networks, restart policies, safe updates and backups with examples.

  • appsgit editors
  • Published
  • 6 min read

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-plugin package. Your distribution's own docker.io package 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:

  • services lists containers. The key (uptime-kuma) is the service name, which also becomes its hostname on the project's network.
  • image is what to run. Pin at least the major version (:2) instead of latest, so a breaking release never arrives by surprise.
  • ports maps host port to container port. Prefixing 127.0.0.1: keeps the app reachable only from the server itself, ready for a reverse proxy.
  • volumes keeps 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/data plus a top-level volumes: 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

  1. Read the release notes for breaking changes, especially major versions.
  2. Back up first (see below).
  3. Run docker compose pull, then docker compose up -d. Compose recreates only containers whose image changed.
  4. 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, .env and 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.

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.

FAQ

Questions and answers

Still curious? Email info@appsgit.com.

How do I install Docker Compose?

On Linux, install Docker Engine from Docker's official apt or dnf repository and include the docker-compose-plugin package. Docker Desktop on Windows and macOS already includes it. Check with docker compose version. The old standalone docker-compose command is legacy.

What is the difference between .env and env_file in Docker Compose?

A .env file in the project folder is read by Compose itself to fill in ${VARIABLES} inside compose.yaml. The env_file key passes variables from a file into the container's environment. Many self-hosted apps use both: .env for settings like versions and paths, env_file or environment for the app.

Should I name the file compose.yaml or docker-compose.yml?

Compose accepts both. compose.yaml is the preferred name in Docker's current documentation, and docker-compose.yml still works. The top-level version key is obsolete and can be removed.

How do I update containers with Docker Compose?

Read the release notes, then run docker compose pull followed by docker compose up -d in the project folder. Compose recreates only the containers whose image changed. Pin major versions in image tags so upgrades happen when you choose.

The weekly digest

Liked this? Get the next one by email

Fresh releases, rising projects and one deploy guide a week. Join home labbers and engineers who self-host.

One email a week. No spam, unsubscribe anytime.