Docker for Homelab — Complete Setup Guide for Self-Hosters
Docker for Homelab — Complete Setup Guide for Self-Hosters
You need Docker running reliably in your homelab, but you're tired of cobbled-together install scripts and want a setup that actually survives reboots. This guide is for self-hosters running bare metal or VMs who want production-grade container orchestration without Kubernetes complexity.
Prerequisites
You'll need:
- Ubuntu 24.04 LTS or Debian 12.x (or equivalent — this guide targets x86-64)
- sudo access on your homelab machine
- Docker 26.1.3+ and Docker Compose 2.28.0+ (we'll verify versions after install)
- 4GB RAM minimum; 8GB+ recommended if running 5+ containers
- 30GB free disk space for images and volumes
- A text editor — nano, vim, or your preferred choice
On my T5810 with 24GB RAM running Ubuntu 24.04, I've stabilized around 12 concurrent containers with minimal overhead.
Installing Docker Engine and Compose
The official Docker repository is the only supported path for homelab use. Skip the snap package — it causes volume mount headaches.
First, remove any existing Docker packages:
sudo apt-get remove docker.io docker-doc docker-compose podman-docker containerd runc 2>/dev/null
sudo apt-get updateInstall Docker's GPG key and repository:
sudo apt-get install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get updateInstall Docker Engine, CLI, containerd, and Compose plugin:
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerify installation:
docker --version
docker compose versionGotcha #1: Docker Compose v2 is installed as a plugin (you run `docker compose`, not `docker-compose`). Old tutorials using the hyphenated command will fail silently. Create an alias if you're stubborn: `echo "alias docker-compose='docker compose'" >> ~/.bashrc`
Enable the Docker daemon to start on boot:
sudo systemctl enable docker
sudo systemctl start dockerConfiguring User Permissions and Networking
Running docker commands with sudo every time is tedious and unsafe. Add your user to the docker group:
sudo usermod -aG docker $USER
newgrp dockerVerify you can run docker without sudo:
docker psGotcha #2: If you `sudo su` into another user, you'll lose docker group membership. Always use `sudo -u username docker` if you need to switch contexts.
Configure Docker daemon settings for homelab stability. Create `/etc/docker/daemon.json`:
sudo nano /etc/docker/daemon.json{
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"ipv6": false,
"live-restore": true,
"userland-proxy": false
}Restart the daemon:
sudo systemctl restart dockerKey settings explained: overlay2 is faster than devicemapper; log rotation prevents disk bloat; live-restore keeps containers running during daemon restarts; userland-proxy: false bypasses iptables overhead on high-traffic homelab setups.
Setting Up Docker Compose for Homelab Services
Docker Compose transforms ugly run commands into declarative YAML. Create a directory structure for your self-hosted services:
mkdir -p ~/homelab/{services,data,backups}
cd ~/homelabCreate a base `docker-compose.yml` that you'll extend for each service:
version: '3.9'
services:
# Services go here
volumes:
# Named volumes go here
networks:
homelab:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16This gives you a dedicated network for container-to-container communication without exposing services to your LAN by default.
Create a `.env` file for environment variables (never hardcode credentials):
cat > ~/homelab/.env << 'EOF'
DOCKER_TIMEZONE=UTC
PUID=1000
PGID=1000
DOMAIN=lab.local
BACKUP_RETENTION_DAYS=30
EOFVerify your setup with a test container:
docker compose -f ~/homelab/docker-compose.yml up -dManaging Volumes and Persistent Storage
Containers are ephemeral. Use volumes for anything you want to keep. In Docker Compose, bind mounts work better than named volumes for homelab setups because they're easier to back up and inspect.
Here's a realistic example stack with Immich (photo library), PostgreSQL, and Nginx:
version: '3.9'
services:
postgres:
image: postgres:16-alpine
container_name: homelab_postgres
restart: unless-stopped
environment:
POSTGRES_DB: immich
POSTGRES_PASSWORD: ${DB_PASSWORD}
PGTZ: ${DOCKER_TIMEZONE}
volumes:
- ./data/postgres:/var/lib/postgresql/data
networks:
- homelab
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
immich-server:
image: ghcr.io/immich-app/immich-server:v1.107.0
container_name: homelab_immich
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DB_HOSTNAME: postgres
DB_USERNAME: postgres
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: immich
REDIS_HOSTNAME: redis
volumes:
- ./data/immich:/usr/src/app/upload
- /mnt/nas/photos:/photos:ro
networks:
- homelab
ports:
- "3001:3001"
nginx:
image: nginx:1.27-alpine
container_name: homelab_nginx
restart: unless-stopped
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
- ./data/certbot:/etc/letsencrypt:ro
ports:
- "80:80"
- "443:443"
networks:
- homelab
depends_on:
- immich-server
volumes:
postgres_data:
driver: local
networks:
homelab:
driver: bridgeKey patterns here:
depends_onwithcondition: service_healthyensures PostgreSQL is ready before Immich starts./data/paths are bind mounts to the filesystem — you can back these up with rsync:romounts are read-only, protecting source data- Pinned image versions (`:v1.107.0`) prevent surprise breakage
To use this stack, add your database password to `.env`:
echo "DB_PASSWORD=$(openssl rand -base64 24)" >> ~/homelab/.envBring the stack up:
cd ~/homelab
docker compose up -dCheck logs and health:
docker compose logs -f immich-server
docker compose psNetworking: Exposing Services Safely
Most homelab self-hosters run everything on a private network. Use bridge networks to isolate services, then expose only what needs exposure via port mapping.
For reverse proxy setups (Nginx/Traefik), don't expose individual services on high ports. Instead, route everything through the proxy:
services:
app:
image: myapp:latest
networks:
- homelab
# NO ports: section — traffic comes through nginx only
environment:
- VIRTUAL_HOST=app.lab.localYour Nginx config then uses Docker's internal DNS:
upstream app {
server homelab_app:8080;
}
server {
listen 80;
server_name app.lab.local;
location / {
proxy_pass http://app;
}
}This is far cleaner than managing dozens of port mappings and gives you centralized TLS termination.
Common Issues and Troubleshooting
Issue: Containers can't connect to each other despite being on the same network.
Check that container names are resolvable:
docker exec -it homelab_app nslookup postgresIf it fails, verify the containers are on the same network:
docker network inspect homelabIf the postgres container isn't listed, add it explicitly in your compose file:
postgres:
networks:
- homelabIssue: Docker daemon crashes on reboot and containers don't restart.
Make sure you set restart: unless-stopped on every service. Never rely on manual restarts in a homelab — you'll forget and have a dead service.
Also verify the daemon started:
sudo systemctl status docker
sudo journalctl -u docker -n 50Issue: Volume permission errors (permission denied writing to /data).
This happens when the container's user (often UID 1000) doesn't match your host user. Fix it:
sudo chown -R 1000:1000 ~/homelab/dataOr set PUID/PGID in your compose file if the image supports it (common in linuxserver.io containers).
Issue: Out of disk space — Docker images and layers filling /var/lib/docker.
Prune aggressively:
docker system prune -a --volumesThis removes all dangling images, containers, and volumes. Use with caution — it's destructive.
Monitor disk usage:
docker system dfSecuring Your Docker Homelab
Run containers as non-root when possible. Avoid --privileged unless absolutely necessary. Use read-only root filesystems where feasible:
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp
- /runDon't expose the Docker socket to containers unless they need to manage other containers. If you must, use /run/user/1000/docker.sock with careful permission management.
Keep images up to date. Set a monthly reminder to rebuild and test your compose stacks:
docker compose pull
docker compose up -dWhat You Now Have
You've got Docker 26.1.3+ installed and running as a non-root user, daemon.json optimized for homelab workloads, Docker Compose v2 configured with reproducible service definitions, persistent storage with bind mounts, and a working example stack ready to extend.
Next steps: commit your compose files to git for version control, set up automated daily backups of your ~/homelab/data directory using rsync or restic, and add Portainer or Dockge if you want a web UI for management.
For specific services, check out guides for Immich, Vaultwarden, and linuxserver.io images — they're the backbone of most homelab self-hosted stacks.