Why Docker Compose?
Docker Compose turns a YAML file into a full application stack. For homelabbers, it means:
- One-command deployments:
docker compose up -d - Version-controlled infrastructure: Track every service in Git
- Predictable networking: Internal DNS and isolated bridge networks by default
- Easy backups: Volume mounts are simple to snapshot and restore
- Declarative configuration: Your entire stack lives in a single YAML file
If you’re running Proxmox or a bare-metal server, Docker Compose is the fastest path from a fresh OS to a working homelab. This guide covers 10 essential stacks plus networking, security, and backup best practices.
Prerequisites
- A Linux host (Debian/Ubuntu recommended) or an LXC container with nesting enabled
- Docker Engine installed: https://docs.docker.com/engine/install/
- Docker Compose plugin (now bundled with Docker Engine)
Verify your setup:
docker --version
docker compose version
Prerequisite: Directory Layout
Before diving into stacks, establish a clean directory structure:
mkdir -p ~/homelab/{nginx-proxy-manager,jellyfin,nextcloud,vaultwarden,pihole,uptime-kuma,homeassistant,qbittorrent,gitea,homepage,scripts}
cd ~/homelab
touch docker-compose.yml .env
Add your sensitive values to .env (never commit this file to Git):
# .env file
DOMAIN=homelab.example.com
PIHOLE_PASSWORD=changeme
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=secure_random_password
TZ=Asia/Jakarta
Stack 1: Reverse Proxy (Nginx Proxy Manager)
Every homelab needs a reverse proxy to expose services securely with SSL.
services:
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "80:80"
- "81:81"
- "443:443"
volumes:
- ./nginx-proxy-manager/data:/data
- ./nginx-proxy-manager/letsencrypt:/etc/letsencrypt
Access the UI at http://<host-ip>:81. Default login: [email protected] / changeme. Change this immediately on first login.
Stack 2: Media Server (Jellyfin)
Host your movies, TV shows, and music without subscription fees.
services:
jellyfin:
image: jellyfin/jellyfin:latest
restart: unless-stopped
ports:
- "8096:8096"
volumes:
- ./jellyfin/config:/config
- ./jellyfin/cache:/cache
- /path/to/media:/media:ro
devices:
- /dev/dri:/dev/dri # Hardware transcoding (Intel QuickSync)
Hardware transcoding requires mapping /dev/dri. For NVIDIA GPUs, add deploy.resources.reservations.devices instead.
Stack 3: Personal Cloud (Nextcloud)
Replace Google Drive with your own cloud. This stack includes MariaDB and Redis for production readiness:
services:
nextcloud-db:
image: mariadb:11
restart: unless-stopped
volumes:
- ./nextcloud/db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${NEXTCLOUD_DB_ROOT_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_PASSWORD=${NEXTCLOUD_DB_PASSWORD}
nextcloud-redis:
image: redis:alpine
restart: unless-stopped
nextcloud:
image: nextcloud:latest
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./nextcloud/data:/var/www/html
environment:
- MYSQL_HOST=nextcloud-db
- REDIS_HOST=nextcloud-redis
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
depends_on:
- nextcloud-db
- nextcloud-redis
Stack 4: Password Manager (Vaultwarden)
A lightweight Bitwarden-compatible server.
services:
vaultwarden:
image: vaultwarden/server:latest
restart: unless-stopped
ports:
- "4743:80"
volumes:
- ./vaultwarden/data:/data
environment:
- WEBSOCKET_ENABLED=true
- SIGNUPS_ALLOWED=false # Disable after creating your account
- ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN} # Admin panel access
After creating your account, set SIGNUPS_ALLOWED=false to prevent public registration.
Stack 5: Network Ad Blocker (Pi-hole)
Block ads across your entire network.
services:
pihole:
image: pihole/pihole:latest
restart: unless-stopped
ports:
- "53:53/tcp"
- "53:53/udp"
- "8081:80"
volumes:
- ./pihole/etc-pihole:/etc/pihole
- ./pihole/etc-dnsmasq.d:/etc/dnsmasq.d
environment:
- TZ=${TZ}
- WEBPASSWORD=${PIHOLE_PASSWORD}
- PIHOLE_DNS_=1.1.1.1;8.8.8.8
Important: Pi-hole listens on port 53, which may conflict with systemd-resolved. Disable it with:
sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
Stack 6: Uptime Monitor (Uptime Kuma)
Know when your services go down before your users do.
services:
uptime-kuma:
image: louislam/uptime-kuma:latest
restart: unless-stopped
ports:
- "3001:3001"
volumes:
- ./uptime-kuma:/app/data
Add monitors for all your homelab services, your internet connection, and your reverse proxy. Configure notification channels (Telegram, email, Slack) so you get alerts on failures.
Stack 7: Smart Home Hub (Home Assistant)
Control Zigbee, Z-Wave, and Wi-Fi devices from one dashboard.
services:
homeassistant:
image: homeassistant/home-assistant:stable
restart: unless-stopped
ports:
- "8123:8123"
volumes:
- ./homeassistant:/config
privileged: true
environment:
- TZ=${TZ}
For USB Zigbee/Z-Wave dongles, add devices: mapping similar to the Jellyfin example. For ESPHome integration, create a dedicated network.
Stack 8: Download Orchestrator (qBittorrent + VPN)
Route torrent traffic through a VPN container to protect your privacy.
services:
gluetun:
image: qmcgaw/gluetun:latest
cap_add:
- NET_ADMIN
volumes:
- ./gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WG_PRIVATE_KEY}
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
restart: unless-stopped
network_mode: service:gluetun # All traffic goes through VPN
volumes:
- ./qbittorrent:/config
- /path/to/downloads:/downloads
Replace mullvad with your provider (Proton VPN, AirVPN, etc.). The network_mode: service:gluetun ensures qBittorrent’s traffic routes exclusively through the VPN — a leak kills the connection.
Stack 9: Git Server (Gitea)
Self-host your repositories without GitLab’s resource hunger.
services:
gitea:
image: gitea/gitea:latest
restart: unless-stopped
ports:
- "3000:3000"
- "222:22"
volumes:
- ./gitea:/data
environment:
- USER_UID=1000
- USER_GID=1000
- DOMAIN=${DOMAIN}
- SSH_DOMAIN=${DOMAIN}
Gitea’s web UI is at port 3000 and SSH at port 222 (non-standard to avoid conflict with the host’s SSH).
Stack 10: Homelab Dashboard (Homepage)
A single pane of glass for all your services. Homepage auto-discovers Docker containers and displays them with live status.
services:
homepage:
image: ghcr.io/gethomepage/homepage:latest
restart: unless-stopped
ports:
- "3002:3000"
volumes:
- ./homepage:/app/config
Configure homepage/services.yaml to add custom links to your services:
# ~/homelab/homepage/services.yaml
- Homelab:
- Nginx Proxy Manager:
icon: nginx-proxy-manager
href: http://192.168.1.10:81
description: Reverse proxy admin
- Vaultwarden:
icon: vaultwarden
href: https://vault.yourdomain.com
description: Password manager
Networking: Custom Bridge Networks
By default, all services in a docker-compose.yml share a network. For better isolation, create custom networks:
# Add to any stack to isolate sensitive services
services:
homeassistant:
...
networks:
- smart-home
- default
networks:
smart-home:
driver: bridge
internal: true # No external internet access for IoT devices
Backup Strategy
Never lose your Docker volumes. Create a backup script:
#!/bin/bash
# ~/homelab/scripts/backup-volumes.sh
BACKUP_DIR="/mnt/backups/docker"
DATE=$(date +%Y%m%d)
mkdir -p "$BACKUP_DIR/$DATE"
for dir in ~/homelab/*/; do
name=$(basename "$dir")
if [ "$name" = "scripts" ]; then continue; fi
tar czf "$BACKUP_DIR/$DATE/$name.tar.gz" -C "$dir" .
done
# Keep only 30 days of backups
find "$BACKUP_DIR" -type d -mtime +30 -exec rm -rf {} \;
Add a nightly cron job: 0 3 * * * bash ~/homelab/scripts/backup-volumes.sh
Security Hardening
- Use a
.envfile for passwords and API keys. Reference them in compose with${VAR_NAME}. Add.envto.gitignore. - Restart policies: Always set
restart: unless-stoppedso services survive reboots. - Never expose admin UIs directly — always put them behind your reverse proxy with authentication.
- Keep images updated:
bash # Weekly update script docker compose pull docker compose up -d docker image prune -f - Resource limits: Prevent a runaway container from starving other services:
yaml services: nextcloud: deploy: resources: limits: memory: 2G cpus: '2.0'
Troubleshooting
Container won’t start
Check logs: docker compose logs <service-name>. Common issues:
- Port conflicts: sudo netstat -tulpn | grep <port>
- Volume permission errors: chown -R 1000:1000 ./service-directory
DNS not resolving inside container
Ensure your docker-compose.yml has dns: 1.1.1.1 or remove custom DNS settings to inherit from the host.
VPN container leaks
Verify the kill switch: docker exec gluetun curl ifconfig.me. If your real IP appears, check the VPN config and restart.
Conclusion
With these 10 Docker Compose stacks, you can bootstrap a complete homelab in under an hour. Start with the reverse proxy and dashboard, then add services one by one. Each stack is self-contained, version-controlled, and portable — perfect for learning, iterating, and scaling.
Remember: start small, back up your volumes, and never expose admin panels to the public internet without authentication.