A Docker Compose Starter Stack for Your First Self-Hosted Setup
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
| Time | Task |
|---|---|
| 0:00 | Provision the server, install Docker, point DNS |
| 0:30 | Bring up Caddy, confirm HTTPS on a test subdomain |
| 1:00 | Add your first app, wire it into the Caddyfile |
| 1:30 | Write and run backup.sh, push one archive off-site |
| 2:00 | Restore that archive onto a throwaway server to prove it works |
| 2:45 | Add 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.
How to Migrate Off Google Workspace Without Breaking Your Week
A calm, step-by-step approach to moving email, docs, calendar, and files away from Google Workspace to open-source and self-hosted tools, without a risky big-bang cutover.
Self-Hosting vs. SaaS in 2026: The Real Cost Breakdown
Self-hosting looks free until you add up your time. Here is a practical way to compare the true cost of running open-source software yourself versus paying for a SaaS subscription.