Back to blog
Self-Hosting
Docker

A Docker Compose Starter Stack for Your First Self-Hosted Setup

Mohsen Karimi4 min read
A Docker Compose Starter Stack for Your First Self-Hosted Setup header image

Most self-hosting guides jump straight to "install this cool app." Then you have ten apps, no HTTPS, no backups, and no idea which one filled the disk. This post is about the boring layer underneath: the four things worth setting up before the apps, so everything you add later is easy.

You need a small server (a $6 to $20 per month virtual machine is plenty to start), a domain name, and Docker with the Compose plugin installed.

The shape of the setup

One directory holds everything:

~/stack/
  docker-compose.yml      # the reverse proxy + shared config
  .env                    # secrets and settings, never committed
  apps/                   # one compose file per app
  data/                   # persistent volumes live here
  backups/                # local backup staging

Keeping all persistent data under one data/ directory is the single most useful habit. When backup time comes, you know exactly what to copy.

Piece 1: A reverse proxy with automatic HTTPS

This is the front door. Every request hits the proxy, which terminates TLS and forwards to the right app based on the hostname. Caddy is the gentlest option because it gets certificates from Let's Encrypt automatically with no configuration.

# docker-compose.yml
services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data/caddy:/data
    networks:
      - web

networks:
  web:
    name: web

The Caddyfile starts almost empty and grows one block per app:

wiki.example.com {
    reverse_proxy wiki:3000
}

Point a wildcard DNS record (*.example.com) at your server's IP and every new subdomain just works. HTTPS is handled for you, renewals included.

Piece 2: A shared network so apps can find each other

Notice the networks: web above, declared as an external network. Every app you add joins the same network, so the proxy can reach it by container name and you never expose app ports directly to the internet.

Each app gets its own compose file under apps/:

# apps/wiki.yml
services:
  wiki:
    image: requarks/wiki:2
    restart: unless-stopped
    environment:
      DB_TYPE: sqlite
    volumes:
      - ../data/wiki:/wiki/data
    networks:
      - web

networks:
  web:
    external: true

Start it with docker compose -f apps/wiki.yml up -d, add the block to your Caddyfile, reload Caddy, done. That is the entire process for every future app.

Piece 3: Backups that you have actually tested

A backup you have never restored is a hope, not a backup. Keep it simple: stop nothing, snapshot the data/ directory, and push it somewhere off the server.

#!/usr/bin/env bash
# backup.sh
set -euo pipefail

STAMP=$(date +%Y-%m-%d)
ARCHIVE="backups/stack-$STAMP.tar.gz"

tar czf "$ARCHIVE" data/
rclone copy "$ARCHIVE" remote:my-backups/
find backups/ -name 'stack-*.tar.gz' -mtime +14 -delete

Run it nightly with cron. For databases that do not like being copied while running (Postgres, MySQL), add a docker compose exec dump command before the tar line and back up the dump file.

Then, once, do the thing everyone skips: spin up a second server, restore the archive, and confirm the apps come back. Put a reminder in your calendar to repeat that every few months.

Piece 4: Controlled updates

Automatic updates that fire whenever an image changes will eventually break something at a bad time. A middle ground: get notified about updates and apply them on your schedule.

The tools in this space can either update containers automatically or run in monitor-only mode. Start in monitor-only. Once a week, read what changed, then pull and recreate:

docker compose -f apps/wiki.yml pull
docker compose -f apps/wiki.yml up -d

Because your data lives in ../data/wiki and not inside the container, recreating the container is safe. That separation is the whole reason updates stop being scary.

An afternoon plan

TimeTask
0:00Provision the server, install Docker, point DNS
0:30Bring up Caddy, confirm HTTPS on a test subdomain
1:00Add your first app, wire it into the Caddyfile
1:30Write and run backup.sh, push one archive off-site
2:00Restore that archive onto a throwaway server to prove it works
2:45Add update notifications, in monitor-only mode

After this, adding a self-hosted app is a five-minute job: one compose file, one Caddyfile block, and it is already backed up because its data sits in the directory you are already copying. The boring layer is what makes the fun part sustainable.

Related posts

More on Self-Hosting, Docker.