Docker on Raspberry Pi: Complete Setup and Project Guide
The Raspberry Pi is the ideal platform for learning Docker and running self-hosted services. With the Pi 5's quad-core ARM Cortex-A76 at 2.4 GHz and up to 8 GB of RAM, it has enough power to run a dozen or more containers simultaneously while consuming under 10 watts. This guide covers everything from initial Docker installation to deploying a complete home lab stack with monitoring and management.
Hardware Recommendations
| Component | Raspberry Pi 4 | Raspberry Pi 5 | Notes |
|---|---|---|---|
| CPU | Cortex-A72 1.8 GHz | Cortex-A76 2.4 GHz | Pi 5 is ~2-3x faster per core |
| RAM | 2/4/8 GB | 4/8 GB | Get 8 GB for Docker workloads |
| Storage | microSD, USB 3.0 | microSD, USB 3.0, NVMe (HAT) | NVMe highly recommended for Pi 5 |
| Network | Gigabit Ethernet | Gigabit Ethernet | Use wired for servers, not Wi-Fi |
| Power | 5V/3A USB-C | 5V/5A USB-C | Use official power supply |
Installing Docker
Start with a fresh Raspberry Pi OS (64-bit) installation. The 64-bit version is essential for running ARM64 Docker images, which have much better availability than 32-bit ARM variants.
# Update the system
sudo apt update && sudo apt upgrade -y
# Install Docker using the official convenience script
curl -fsSL https://get.docker.com | sh
# Add your user to the docker group (avoids sudo for every command)
sudo usermod -aG docker $USER
# Log out and back in for group changes to take effect
# Then verify the installation
docker version
docker run --rm hello-world
Install Docker Compose
# Docker Compose v2 is included with Docker Engine as a plugin
# Verify it works
docker compose version
# If not present, install the plugin
sudo apt install docker-compose-plugin
Move Docker Storage to SSD
If you are using an external SSD, move Docker's data directory off the microSD card:
# Stop Docker
sudo systemctl stop docker
# Mount your SSD (example: /dev/sda1 formatted as ext4)
sudo mkdir -p /mnt/ssd
sudo mount /dev/sda1 /mnt/ssd
# Add to /etc/fstab for persistence
echo "/dev/sda1 /mnt/ssd ext4 defaults,noatime 0 2" | sudo tee -a /etc/fstab
# Move Docker data
sudo rsync -aP /var/lib/docker/ /mnt/ssd/docker/
# Configure Docker to use the new location
sudo tee /etc/docker/daemon.json <<EOF
{
"data-root": "/mnt/ssd/docker",
"storage-driver": "overlay2",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
# Start Docker
sudo systemctl start docker
# Verify
docker info | grep "Docker Root Dir"
# Expected: /mnt/ssd/docker
ARM64 Image Compatibility
Most popular Docker images now support ARM64 natively through multi-architecture manifests. When you docker pull an image, Docker automatically selects the correct architecture.
# Check if an image supports ARM64
docker manifest inspect library/nginx:alpine | \
jq '.manifests[] | select(.platform.architecture=="arm64")'
# Common images with ARM64 support:
# nginx, postgres, redis, mariadb, node, python, golang,
# traefik, grafana, prometheus, pihole, homeassistant,
# nextcloud, vaultwarden, jellyfin, caddy
Essential Self-Hosted Services
Pi-hole: Network-Wide Ad Blocking
# docker-compose.yml for Pi-hole
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
ports:
- "53:53/tcp"
- "53:53/udp"
- "8080:80/tcp"
environment:
TZ: "America/New_York"
WEBPASSWORD: "${PIHOLE_PASSWORD}"
FTLCONF_LOCAL_IPV4: "192.168.1.100"
volumes:
- pihole_data:/etc/pihole
- pihole_dnsmasq:/etc/dnsmasq.d
restart: unless-stopped
dns:
- 127.0.0.1
- 1.1.1.1
volumes:
pihole_data:
pihole_dnsmasq:
Home Assistant: Smart Home Hub
services:
homeassistant:
container_name: homeassistant
image: ghcr.io/home-assistant/home-assistant:stable
volumes:
- ha_config:/config
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
privileged: true
network_mode: host
# network_mode: host is needed for mDNS discovery
# of smart home devices on the local network
volumes:
ha_config:
Nextcloud: Personal Cloud Storage
services:
nextcloud:
image: nextcloud:latest
container_name: nextcloud
ports:
- "8443:443"
environment:
MYSQL_HOST: nextcloud-db
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: "${NEXTCLOUD_DB_PASS}"
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: "${NEXTCLOUD_ADMIN_PASS}"
NEXTCLOUD_TRUSTED_DOMAINS: "nextcloud.local 192.168.1.100"
volumes:
- nextcloud_data:/var/www/html
- /mnt/ssd/nextcloud-files:/var/www/html/data
depends_on:
- nextcloud-db
restart: unless-stopped
nextcloud-db:
image: mariadb:11
container_name: nextcloud-db
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASS}"
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: "${NEXTCLOUD_DB_PASS}"
volumes:
- nextcloud_db:/var/lib/mysql
restart: unless-stopped
volumes:
nextcloud_data:
nextcloud_db:
Media Server Stack
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
ports:
- "8096:8096"
volumes:
- jellyfin_config:/config
- jellyfin_cache:/cache
- /mnt/ssd/media:/media:ro
environment:
JELLYFIN_PublishedServerUrl: "http://192.168.1.100:8096"
restart: unless-stopped
# Hardware transcoding on Pi 5 (V4L2)
devices:
- /dev/video10:/dev/video10
- /dev/video11:/dev/video11
- /dev/video12:/dev/video12
volumes:
jellyfin_config:
jellyfin_cache:
Complete Home Lab Stack
Here is a curated Docker Compose stack that runs well on a Raspberry Pi 5 with 8 GB RAM:
# homelab/docker-compose.yml
services:
# Reverse Proxy
traefik:
image: traefik:v3.0
container_name: traefik
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_certs:/certs
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
restart: unless-stopped
# Password Manager
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
environment:
DOMAIN: "https://vault.local"
SIGNUPS_ALLOWED: "false"
volumes:
- vaultwarden_data:/data
labels:
traefik.enable: "true"
traefik.http.routers.vault.rule: "Host(`vault.local`)"
restart: unless-stopped
# Monitoring
uptime-kuma:
image: louislam/uptime-kuma:latest
container_name: uptime-kuma
ports:
- "3001:3001"
volumes:
- uptime_kuma:/app/data
restart: unless-stopped
# Bookmarks
linkding:
image: sissbruecker/linkding:latest
container_name: linkding
ports:
- "9090:9090"
volumes:
- linkding_data:/etc/linkding/data
restart: unless-stopped
volumes:
traefik_certs:
vaultwarden_data:
uptime_kuma:
linkding_data:
Performance Expectations
| Scenario | Pi 4 (8GB) | Pi 5 (8GB) |
|---|---|---|
| Idle Docker daemon | ~80 MB RAM | ~80 MB RAM |
| 5 lightweight containers | ~500 MB RAM, 5% CPU | ~500 MB RAM, 3% CPU |
| 10 mixed containers | ~2-3 GB RAM, 15% CPU | ~2-3 GB RAM, 10% CPU |
| Postgres under load | ~100 queries/sec | ~250 queries/sec |
| Docker image build (Go app) | ~4 minutes | ~1.5 minutes |
| Jellyfin transcoding (1080p) | Struggles (software only) | Acceptable (V4L2 HW) |
Storage Considerations
- microSD cards: Only for boot. Write endurance is too low for Docker's overlay filesystem
- USB 3.0 SSD: Best option for Pi 4. 500 GB SATA SSD with a USB adapter provides excellent performance at low cost
- NVMe SSD: Best option for Pi 5 with the official NVMe HAT. Provides the fastest I/O and lowest latency
- External HDD: Acceptable for media storage and backups, but too slow for Docker data directories or databases
Clustering with Docker Swarm
Multiple Raspberry Pis can form a Docker Swarm cluster for high availability and load distribution:
# On Pi 1 (manager)
docker swarm init --advertise-addr 192.168.1.100
# Join token will be displayed, use it on other Pis:
# On Pi 2 and Pi 3 (workers)
docker swarm join --token SWMTKN-1-xxx 192.168.1.100:2377
# Verify cluster
docker node ls
# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
# abc123 * pi-01 Ready Active Leader
# def456 pi-02 Ready Active
# ghi789 pi-03 Ready Active
# Deploy a service across the cluster
docker service create --name web --replicas 3 -p 80:80 nginx:alpine
Monitoring Your Pi
Monitor your Raspberry Pi's Docker environment with a lightweight Prometheus and Grafana stack:
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
- '--storage.tsdb.retention.size=2GB'
restart: unless-stopped
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
restart: unless-stopped
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
ports:
- "8081:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_PASSWORD}"
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:
cat /sys/class/thermal/thermal_zone0/temp (divide by 1000 for Celsius). Keep it below 80 degrees C.
The Raspberry Pi's low cost, low power consumption, and ARM64 architecture make it an excellent platform for running Docker-based self-hosted services. Start with one or two services, monitor resource usage, and expand your stack as you gain confidence. With proper storage configuration and resource management, a single Pi can comfortably run your personal infrastructure for years.