Linux & Hosting

Docker Compose basics for self-hosting

Updated August 2026

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 .env file next to the compose file (KEY=value per line, referenced as ${KEY}) so the compose file itself is safe to share or version.
  • image tags:latest is 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.