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.
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
Named volumes (recommended)
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.