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

  1. Use a .env file for passwords and API keys. Reference them in compose with ${VAR_NAME}. Add .env to .gitignore.
  2. Restart policies: Always set restart: unless-stopped so services survive reboots.
  3. Never expose admin UIs directly — always put them behind your reverse proxy with authentication.
  4. Keep images updated: bash # Weekly update script docker compose pull docker compose up -d docker image prune -f
  5. 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.