Docker Compose All The Things: Running 10+ Services on One Host
My homelab runs entirely on Docker Compose — no Kubernetes, no Nomad, no Proxmox clustering. Just one Ubuntu Server, a dozen docker-compose.yml files, and a reverse proxy that ties it all together. Here's how each piece fits.
The Architecture
Each service lives in its own directory under ~/, with its own docker-compose.yml. This keeps services isolated, independently restartable, and easy to back up. The bring-up order matters: infrastructure first (Traefik, Pi-hole), then applications.
Infrastructure Layer
Traefik — Reverse Proxy + Local CA TLS
Traefik is the front door. It routes *.homelab domains to the right container and terminates TLS using a self-signed wildcard certificate from a local CA I generate with OpenSSL:
openssl genrsa -out ca.key 4096 openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \ -subj "/CN=Homelab Local CA" openssl genrsa -out wildcard.key 2048 openssl req -new -key wildcard.key -out wildcard.csr \ -subj "/CN=*.homelab" openssl x509 -req -in wildcard.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out wildcard.crt -days 3650 \ -extfile <(printf "subjectAltName=DNS:*.homelab,DNS:homelab")
Every service gets HTTPS automatically. I trust the CA on my client devices so there are no browser warnings. Health check: curl -k https://traefik.homelab/ping.
Pi-hole — DNS + Ad Blocking
Pi-hole serves as the network's primary DNS. It resolves *.homelab to the host IP and blocks ads network-wide. My router's DHCP hands out the Pi-hole IP as the DNS server, so every device on the network benefits automatically.
Application Layer
Homepage — The Dashboard
Homepage is the landing page with live widgets pulling stats from every service: Traefik routing status, Pi-hole query counts, Plex active streams, Tautulli history, Portainer container counts, and Glances system metrics. The widget config lives in homepage/config/services.yaml, which references API keys for each service.
Plex + Tautulli — Media Server + Analytics
Plex serves my media library. Tautulli monitors who's watching what, tracks history, and provides Plex usage analytics. Both authenticate via tokens stored in their config files (redacted in the backup repo).
Arr Stack — Media Acquisition Pipeline
This is the most complex service group, running five containers via a single docker-compose.yml:
- Prowlarr — Indexer aggregator, feeds search results to Radarr/Sonarr
- Radarr — Movie automation
- Sonarr — TV show automation
- qBittorrent — Torrent client
- Gluetun — VPN container using WireGuard, routing all Arr traffic through the tunnel
The Arr services communicate via Docker DNS (http://prowlarr:9696), and the WireGuard private key is injected at runtime (never stored in Git).
Immich — Photo Backup
Immich is a self-hosted Google Photos alternative, backed by PostgreSQL. The database password is generated on first install and stored in a local .env file that's excluded from version control. Photos sync automatically from my phone.
Home Assistant — Automation Hub
Home Assistant manages smart home devices. Its config directory is backed up, but runtime state and database are excluded. Long-lived access tokens are generated via the web UI and never committed.
Glances — System Metrics
Glances exposes system metrics on port 61208. Homepage's Glances widget pulls CPU, memory, network, and disk stats in real-time. No authentication required for internal access.
Bring-up Order
When rebuilding from scratch, order matters:
- Traefik — Reverse proxy must be up first for routing
- Pi-hole — DNS must be available for
*.homelabresolution - Homepage — Dashboard (can start without backends, widgets show errors until services come up)
- Plex → Tautulli → Arr Stack → Immich → Home Assistant
What Makes This Work
docker-compose.yml. No monolithic stack file. This means I can restart, upgrade, or debug any service without touching the others.
- Docker DNS handles inter-service communication (containers reference each other by name)
- Traefik dynamic config routes domains to services automatically
- UFW firewall limits external access to SSH (22), HTTP (80), HTTPS (443), and DNS (53)
- Static IP + Pi-hole ensures stable, ad-free DNS resolution
Next up: Disaster Recovery in Under an Hour — how I rebuild this entire stack from a clean Ubuntu install.