Run Docker Compose on Raspberry Pi

Use Docker Compose to manage multi-container services on your Raspberry Pi. Covers compose files, networking, volumes, and updates.

Andreas · April 12, 2026 · 8 min read

Introduction

Docker Compose replaces manual docker run commands with a single YAML file. Define multi-container services, networks, volumes, and environment variables once, then deploy with a single command. This guide covers installing Compose on Pi, writing compose files, and managing production stacks with real examples.

Prerequisites

  • Raspberry Pi 4 (2GB+ RAM) or Pi 5
  • Docker installed: curl -fsSL https://get.docker.com | sh
  • SSH access to your Pi
  • Basic YAML familiarity

Step 1 — Install Docker Compose Plugin

Docker now bundles Compose as a plugin. Verify it's included:

docker compose version

If missing, install manually:

sudo curl -L "https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version

Create alias for compatibility:

echo "alias docker-compose='docker compose'" >> ~/.bashrc
source ~/.bashrc

Step 2 — Create Your First Compose File

Create a directory for your stack:

mkdir -p ~/docker/pihole-unbound
cd ~/docker/pihole-unbound
nano docker-compose.yml

Write a simple Pi-hole + Unbound stack:

version: '3.8'

services:
  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
    environment:
      TZ: "UTC"
      WEBPASSWORD: "changeme"
      DNS1: "127.0.0.1#5053"
      DNS2: "no"
    volumes:
      - pihole_config:/etc/pihole
      - dnsmasq_config:/etc/dnsmasq.d
    depends_on:
      - unbound
    restart: unless-stopped
    networks:
      - dns_network

  unbound:
    image: mvance/unbound:latest
    container_name: unbound
    ports:
      - "5053:53/udp"
      - "5053:53/tcp"
    volumes:
      - unbound_config:/opt/unbound/etc/unbound
    restart: unless-stopped
    networks:
      - dns_network

volumes:
  pihole_config:
  dnsmasq_config:
  unbound_config:

networks:
  dns_network:
    driver: bridge

Save with Ctrl+X, then Y.

Step 3 — Launch the Stack

Deploy all services:

docker compose up -d

Verify containers are running:

docker compose ps

Expected output:

NAME                COMMAND             STATUS
pihole              "/s6-init"          Up 2 minutes
unbound             "/sbin/docker-ent…" Up 2 minutes

Step 4 — Manage Services

View logs

docker compose logs pihole
docker compose logs -f unbound    # Follow unbound logs

Stop stack

docker compose stop

Restart stack

docker compose restart

Remove stack (keeps volumes)

docker compose down

Remove stack + all data

docker compose down -v

Step 5 — Update Containers

Pull latest images:

docker compose pull

Restart with new images:

docker compose up -d

Only changed images restart. No downtime for unchanged services.

Environment Variables

Keep secrets outside version control. Create .env file:

nano .env

Add variables:

PIHOLE_PASSWORD=mysecurepass
PIHOLE_TZ=America/Chicago
PIHOLE_DOMAIN=pi.home

Reference in compose:

environment:
  WEBPASSWORD: "${PIHOLE_PASSWORD}"
  TZ: "${PIHOLE_TZ}"

Load with:

docker compose --env-file .env up -d

Important: Never commit .env to git. Add to .gitignore.

Working with Volumes

Automatically created, managed by Docker:

volumes:
  pihole_config:
  unbound_data:

Bind mounts (for config files)

Map local directory into container:

volumes:
  - ./pihole_config:/etc/pihole
  - ./unbound.conf:/opt/unbound/etc/unbound/unbound.conf

List volumes

docker volume ls
docker volume inspect pihole_config

Backup volume

docker run --rm -v pihole_config:/data -v $(pwd):/backup \
  alpine tar czf /backup/pihole-backup.tar.gz -C /data .

Networks

Containers on same compose network auto-discover by service name (DNS). Pi-hole queries Unbound via unbound:5053 automatically.

Create custom network:

networks:
  dns_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

Assign services:

services:
  pihole:
    networks:
      - dns_network

Resource Limits

Prevent runaway containers. Limit CPU and memory:

services:
  pihole:
    image: pihole/pihole:latest
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

Check usage:

docker stats

Production Example: Full Homelab Stack

version: '3.8'

services:
  traefik:
    image: traefik:v2.11
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - traefik_config:/etc/traefik
    restart: unless-stopped

  pihole:
    image: pihole/pihole:latest
    environment:
      WEBPASSWORD: "${PIHOLE_PASSWORD}"
    volumes:
      - pihole_config:/etc/pihole
    depends_on:
      - unbound
    restart: unless-stopped

  unbound:
    image: mvance/unbound:latest
    restart: unless-stopped

  portainer:
    image: portainer/portainer-ce:latest
    ports:
      - "9000:9000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    restart: unless-stopped

  netdata:
    image: netdata/netdata:latest
    ports:
      - "19999:19999"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
    cap_add:
      - SYS_PTRACE
    restart: unless-stopped

volumes:
  traefik_config:
  pihole_config:
  portainer_data:

Troubleshooting

Containers won't start

Check logs: docker compose logs. Look for port conflicts (Address already in use) or missing volumes.

Can't connect between containers

Verify both services are on same network. Check network name: docker network ls and docker network inspect <name>.

Memory/CPU capped

Increase resource limits in deploy.resources section or on host: free -h and df -h to diagnose.

Port conflicts

Change mapping: -p 8080:80 instead of -p 80:80. Verify no other service uses port: sudo lsof -i :80.

Summary

Docker Compose makes managing multi-service homelabs simple and reproducible. Use named volumes for data persistence, environment variables for secrets, and resource limits to protect your Pi. Keep your compose file in git (without .env), and deployments become one-command operations.

Comments