14 min read

Getting Started with Docker Compose: A Home Lab Guide

Docker Compose is the single best tool for managing self-hosted services on your home server. Instead of a wall of docker run flags you'll forget by next week, Compose gives you readable YAML files, one-command deployments, and dead-simple updates. This guide takes you from zero to running real stacks.

Docker Compose running in a terminal with multiple containers starting up

What Is Docker Compose?

Docker Compose is a tool for defining and running multi-container Docker applications. You write a compose.yaml file (previously docker-compose.yml) that describes your services, networks, and volumes. Then a single command brings everything up.

For home lab use this is invaluable. Rather than documenting which ports, environment variables, and volume mounts you used for every container, it's all captured in a version-controlled YAML file that you can commit to Git and redeploy on any machine in minutes.

Compose V2

Docker Compose V2 ships as a plugin with Docker Desktop and modern Docker Engine installations. The command is now docker compose (no hyphen). The old docker-compose standalone binary still exists but V2 is the standard going forward.

Installing Docker & Compose

The fastest way to install Docker Engine with Compose V2 on Debian/Ubuntu (the most common base for home servers and Proxmox LXC containers) is the official install script:

bash
# Install Docker via convenience script (Debian/Ubuntu)
curl -fsSL https://get.docker.com | sh

# Add your user to the docker group (avoid using sudo)
sudo usermod -aG docker $USER

# Apply group change without logging out
newgrp docker

# Verify installation
docker --version
docker compose version

On Fedora/RHEL-based systems:

bash
sudo dnf install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER

Writing Your First Compose File

A Compose file is a YAML document with four main top-level keys: services, volumes, networks, and configs. For most home lab use cases you only need the first three.

Here's a minimal example running Jellyfin — one of the most popular self-hosted media servers:

yaml
# compose.yaml
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    ports:
      - "8096:8096"       # Web UI
      - "8920:8920"       # HTTPS (optional)
    environment:
      - PUID=1000          # Run as your user ID
      - PGID=1000          # Run as your group ID
      - TZ=America/Chicago
    volumes:
      - jellyfin-config:/config
      - /mnt/media:/media:ro   # Your media library (read-only)
    devices:
      - /dev/dri:/dev/dri  # Intel/AMD hardware transcoding (optional)

volumes:
  jellyfin-config:

Save this as ~/jellyfin/compose.yaml and run it:

bash
cd ~/jellyfin
docker compose up -d   # -d = detached (background)
docker compose logs -f # Follow logs
docker compose ps      # Check status

Key Concepts

Volumes

Volumes persist data outside the container lifecycle. There are two kinds you'll use:

  • Named volumes — managed by Docker (jellyfin-config:/config). Stored in /var/lib/docker/volumes/. Good for app config you don't need to browse directly.
  • Bind mounts — a path on your host maps into the container (/mnt/media:/media). Good for data you want to access from the host too (media, backups).

Networks

By default, every Compose project gets its own isolated bridge network. Services in the same Compose file can reach each other by service name. For example, a db service is reachable at hostname db from any other service in that file. This is how you connect a WordPress container to its database container.

Environment Variables

Never hardcode secrets in your Compose file. Use a .env file in the same directory:

.env
POSTGRES_PASSWORD=mysupersecretpassword
PGID=1000
PUID=1000
TZ=America/Chicago
yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}  # Interpolated from .env
Add .env to .gitignore

Your .env file contains secrets. Never commit it to a public Git repo. Add .env to your .gitignore and commit a .env.example with placeholder values instead.

A Real Multi-Service Stack

Here's a practical Compose stack I use daily — Nextcloud with a PostgreSQL database and Redis cache, all wired together on a private network. This is the kind of setup that would require five minutes of copy-pasting flags with plain docker run but deploys cleanly in one command with Compose:

yaml
# compose.yaml — Nextcloud stack
services:
  nextcloud:
    image: nextcloud:latest
    container_name: nextcloud
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - REDIS_HOST=redis
      - NEXTCLOUD_ADMIN_USER=${ADMIN_USER}
      - NEXTCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD}
      - NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.yourdomain.com
    volumes:
      - nextcloud-data:/var/www/html
    depends_on:
      - db
      - redis
    networks:
      - nextcloud-net

  db:
    image: postgres:16-alpine
    container_name: nextcloud-db
    restart: unless-stopped
    environment:
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    volumes:
      - nextcloud-db:/var/lib/postgresql/data
    networks:
      - nextcloud-net

  redis:
    image: redis:7-alpine
    container_name: nextcloud-redis
    restart: unless-stopped
    command: redis-server --save 60 1 --loglevel warning
    volumes:
      - nextcloud-redis:/data
    networks:
      - nextcloud-net

volumes:
  nextcloud-data:
  nextcloud-db:
  nextcloud-redis:

networks:
  nextcloud-net:
    driver: bridge

Essential Compose Commands

These are the commands you'll use every single day. All of them run from the directory containing your compose.yaml:

bash
# Start all services in the background
docker compose up -d

# Stop all services (keeps data)
docker compose down

# Stop AND remove volumes (destroys data — careful!)
docker compose down -v

# Pull latest images then restart
docker compose pull && docker compose up -d

# View logs (all services)
docker compose logs -f

# View logs (specific service)
docker compose logs -f nextcloud

# Open a shell in a running container
docker compose exec nextcloud bash

# Check status of all services
docker compose ps

# Restart a single service
docker compose restart nextcloud

# Rebuild after changing image or build context
docker compose up -d --build

Keeping Containers Updated

Updating containers with Compose is two commands — pull the new image, then restart:

bash
docker compose pull          # Pull all updated images
docker compose up -d         # Recreate containers using new images
docker image prune -f        # Clean up dangling old images
Automate Updates with Watchtower

Add Watchtower as a service in your Compose stack to automatically pull and restart containers when new images are published. Just be careful using it on production-critical services — pin image tags instead of :latest for anything you depend on heavily.

Recommended Hardware for Running Docker at Home

Running Docker Compose at home doesn't require beefy hardware, but the right gear makes a difference. Here's what I recommend and personally use:

Amazon Associate disclosure: Links above are affiliate links. I earn a small commission if you purchase through them at no extra cost to you.

Next Steps

Once you're comfortable with basic Compose stacks, here's where to go next:

  • Nginx Proxy Manager — Put a reverse proxy in front of all your services with automatic SSL certificates.
  • Portainer — A web GUI to manage all your Docker containers and Compose stacks visually.
  • Watchtower — Auto-update containers when new images are published.
  • Migrate to K3s — When you outgrow Compose, Kubernetes (via K3s) is the natural next step for orchestration at scale.