title: “Build Your Own Home Lab: A Complete Guide for Beginners” date: “2026-04-08T00:00:00.000Z” category: “DevOps” tags: [“homelab”, “selfhosted”, “docker”, “traefik”, “cloudflare”]
So you want to run your own server at home. Maybe you’re tired of paying for cloud services. Maybe you want to learn DevOps for real. Maybe you just like owning your data. Whatever the reason — this guide will take you from zero to a fully running home lab, step by step, no prior experience needed.
I built this exact setup on a single machine sitting at home. It runs my personal website, photo library, smart home, cloud storage, media server, and automation workflows — all for free, on hardware I already owned.
By the end of this guide you’ll have the same.
What You Need
Before anything else, let’s talk hardware. You don’t need a rack server or expensive equipment.
My setup: an old desktop PC with an Intel i3, 8 GB RAM, and a 512 GB SSD. Total cost: €0 — I already had it. The only thing I bought was a domain name.
The Big Picture
Before writing a single command, let’s understand what we’re building. Most guides throw config files at you without explaining why. Not this one.
Here’s how everything connects:
The most important thing to understand: you never open any ports on your router. The Cloudflare Tunnel makes an outbound connection from your server to Cloudflare’s network. This means even if your ISP uses CGNAT (which many do) — it still works.
Why Docker?
Every service runs in its own Docker container. Think of containers like this:
App B needs Python 3.11
💥 They conflict. One breaks.
Kill App A → App B might die
Update one → breaks the other
Reinstall OS to fix it
App B in its own box (Python 3.11)
✓ No conflicts. Ever.
Kill App A → App B untouched
Update one → others unaffected
One command to fix anything
Docker also means if something breaks, you just delete the container and start fresh. Your data is safe because it lives outside the container on your disk.
Step 1 — Install Ubuntu Server
Download Ubuntu Server 24.04 LTS from ubuntu.com and flash it to a USB drive using Balena Etcher.
Boot your machine from USB and follow the installer. When it asks about storage, choose Use entire disk with LVM. Everything else can stay default.
After install, expand the LVM to use all available space (Ubuntu’s installer only uses ~100 GB by default):
sudo pvresize /dev/sda3
sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
df -h / # Should show full disk size
Step 2 — Secure the Server
Before installing anything, lock it down:
# Update everything
sudo apt update && sudo apt upgrade -y
# Install essentials
sudo apt install -y curl git ufw fail2ban
# Firewall: deny all inbound except SSH from your home network
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 192.168.1.0/24 to any port 22 # SSH — LAN only
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
Step 3 — Install Docker
Never install Docker from Ubuntu’s package manager — it’s always outdated. Use Docker’s official repository:
# Add Docker's GPG key
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
# Add Docker repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu noble stable" | \
sudo tee /etc/apt/sources.list.d/docker.list
# Install
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
# Add yourself to docker group (no sudo needed for docker commands)
sudo usermod -aG docker $USER
# Log out and back in, then test
docker run hello-world
Ubuntu 24.04 tip: Ubuntu 24.04 may have Snap Docker pre-installed. This causes conflicts. Remove it first:
sudo snap remove docker
Step 4 — Set Up the Directory Structure
Organization matters. Every service gets its own folder:
# Create the structure
sudo mkdir -p /opt/traefik/acme
sudo mkdir -p /opt/cloudflared
sudo mkdir -p /opt/production/site
sudo mkdir -p /opt/production/immich
sudo mkdir -p /opt/internal/homarr
sudo mkdir -p /opt/internal/portainer
sudo mkdir -p /data/backups
# Give yourself ownership
sudo chown -R $USER:$USER /opt
# Create Docker networks
docker network create traefik-public # All web-facing apps connect here
docker network create prod-net # Production databases
docker network create play-net # Playground experiments
/opt/ ├── traefik/ # The front door — all traffic goes through here ├── cloudflared/ # Cloudflare tunnel — connects you to the internet ├── production/ # Live services your visitors see │ ├── site/ # Your main website │ └── immich/ # Photo library ├── internal/ # Admin tools — only you can access │ ├── homarr/ # Dashboard to see all services │ └── portainer/ # Manage containers visually └── playground/ # Experiments — break things here, not in production
Step 5 — Set Up Cloudflare
This is the magic that makes everything work without port forwarding.
5a — Add your domain to Cloudflare
- Sign up at cloudflare.com (free)
- Add your domain → follow the steps to update your nameservers
- Wait 5-10 minutes for DNS to propagate
5b — Create a Tunnel
- Cloudflare dashboard → Zero Trust → Networks → Tunnels
- Create a tunnel → name it
my-homelab - Copy the tunnel token (long string starting with
eyJ...)
5c — Add DNS records
In Cloudflare DNS, add these records pointing to your public IP:
| Type | Name | Content | Proxy |
|---|---|---|---|
| A | @ | your-public-ip | ✅ Proxied |
| A | * | your-public-ip | ✅ Proxied |
The * wildcard means every subdomain automatically works — photos.yourdomain.com, files.yourdomain.com, everything — without adding new DNS records each time.
Find your public IP: run
curl -4 ifconfig.meon the server
5d — Start the tunnel
# /opt/cloudflared/.env
TUNNEL_TOKEN=paste_your_token_here
# /opt/cloudflared/docker-compose.yml
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
network_mode: host
env_file: .env
command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
cd /opt/cloudflared && docker compose up -d
docker logs cloudflared --tail 10
# Should show: Registered tunnel connection
Step 6 — Set Up Traefik
Traefik is the reverse proxy that sits between Cloudflare and your apps. It reads the domain name from incoming requests and routes to the right container.
The genius of Traefik is that you configure routing inside each app’s compose file using labels. No central config to edit, no restarts needed when you add new apps.
# /opt/traefik/traefik.yml
global:
checkNewVersion: false
sendAnonymousUsage: false
api:
dashboard: true
insecure: true
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
providers:
docker:
exposedByDefault: false # Only route containers that opt in
network: traefik-public
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /etc/traefik/acme/acme.json
dnsChallenge:
provider: cloudflare
resolvers: ["1.1.1.1:53"]
touch /opt/traefik/acme/acme.json
chmod 600 /opt/traefik/acme/acme.json
cd /opt/traefik && docker compose up -d
# Access dashboard at http://YOUR-LOCAL-IP:8080/dashboard/
Step 7 — Deploy Your First App
Now the fun part. Let’s deploy a website. Every app follows this same pattern:
labels: - "traefik.enable=true" # opt in to Traefik routing - "traefik.http.routers.APPNAME.rule=Host(`sub.domain.com`)" # which domain - "traefik.http.routers.APPNAME.entrypoints=websecure" # HTTPS only - "traefik.http.services.APPNAME-svc.loadbalancer.server.port=3000" # container port
Here’s a complete example — a simple website:
# /opt/production/site/docker-compose.yml
services:
site:
image: nginx:alpine
container_name: my-site
restart: unless-stopped
volumes:
- ./index.html:/usr/share/nginx/html/index.html:ro
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.site.rule=Host(`yourdomain.com`)"
- "traefik.http.routers.site.entrypoints=websecure"
- "traefik.http.routers.site.tls.certresolver=letsencrypt"
- "traefik.http.routers.site.service=site-svc"
- "traefik.http.routers.site-http.rule=Host(`yourdomain.com`)"
- "traefik.http.routers.site-http.entrypoints=web"
- "traefik.http.routers.site-http.service=site-svc"
- "traefik.http.services.site-svc.loadbalancer.server.port=80"
networks:
traefik-public:
external: true
Then tell Cloudflare to route this domain through the tunnel:
Cloudflare → Zero Trust → Tunnels → your tunnel → Public Hostnames → Add
| Subdomain | Domain | Type | URL |
|---|---|---|---|
| (empty) | yourdomain.com | HTTP | 192.168.1.X:80 |
cd /opt/production/site && docker compose up -d
# Your site is now live at https://yourdomain.com
What You Can Run
Once the foundation is in place, adding new services takes about 5 minutes each. Here’s what I run:
The Playground Zone
One of the best features of this setup is having a dedicated “playground” — a separate area where you can experiment without risking your production services.
# Playground apps use a different subdomain pattern
play.yourdomain.com # main playground entry
myexperiment.play.yourdomain.com # individual experiments
The playground uses a separate Docker network (play-net) so playground databases are completely isolated from production. You can destroy the entire playground without touching a single production service.
# Playground compose file pattern
services:
myexperiment:
image: whatever:latest
restart: "no" # Don't auto-restart playground apps
networks:
- traefik-public
- play-net # Isolated from prod-net
Auto-Deploy with GitHub Actions
Once everything runs, you want changes to deploy automatically when you push code. Here’s how:
Create a deploy key on your server and add it to GitHub Secrets:
# Generate deploy key
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions -N ""
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
# Add to Cloudflare tunnel for SSH access
# Cloudflare → Zero Trust → Tunnels → your tunnel → Public Hostnames
# ssh.yourdomain.com → SSH → localhost:22
Then add to GitHub repo → Settings → Secrets:
| Secret | Value |
|---|---|
SSH_PRIVATE_KEY | Contents of ~/.ssh/github_actions |
SSH_HOST | ssh.yourdomain.com |
SSH_USER | your username |
Your .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Install cloudflared
run: |
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
sudo mv cloudflared /usr/local/bin/cloudflared && sudo chmod +x /usr/local/bin/cloudflared
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
cat >> ~/.ssh/config << 'EOF'
Host ssh.yourdomain.com
StrictHostKeyChecking no
ProxyCommand cloudflared access ssh --hostname %h
EOF
- name: Deploy
run: |
ssh ${{ secrets.SSH_USER }}@ssh.yourdomain.com << 'DEPLOY'
cd /opt/production/myapp
git pull origin main
docker compose up -d --force-recreate
DEPLOY
Push to main → your server updates automatically. No SSH needed, no manual steps.
Common Problems & Fixes
Useful Commands
# Check all running containers
docker ps --format "table {{.Names}}\t{{.Status}}"
# View logs for any service
docker logs traefik --tail 50 -f
docker logs immich-server --tail 50 -f
# Restart a service
cd /opt/production/myapp && docker compose restart
# Update a service to latest image
docker compose pull && docker compose up -d
# Check disk usage
df -h / && docker system df
# Clean up unused images (free disk space)
docker image prune -f
# See what Traefik is routing
curl -s http://localhost:8080/api/http/routers | python3 -m json.tool
What’s Next
Once your home lab is running, the rabbit hole goes deep. Some ideas:
- Gitea — self-hosted GitHub for private repos
- Vaultwarden — self-hosted Bitwarden password manager
- Uptime Kuma — beautiful monitoring dashboard with alerts
- Grafana + Prometheus — server metrics and dashboards
- WireGuard — VPN to access your LAN from anywhere
- Tailscale — even easier VPN, zero config
The beauty of this setup is that adding any of these takes the same 5 minutes: create a folder, write a docker-compose.yml, add Traefik labels, start it up. That’s it.
Final Thoughts
Running a home lab teaches you more about infrastructure, networking, and DevOps than any course or certification. You’ll break things. You’ll fix them. You’ll understand why they broke. That’s the point.
The setup described here costs nothing beyond the hardware you already have. Everything runs on free software. Your data is yours, on your machine, under your control.
Start small. Get Traefik running. Deploy one app. Then add another. Before long you’ll have a full production-grade infrastructure running at home — and you’ll understand every piece of it because you built it yourself.
All config files and scripts from this guide are available in my GitHub repos: github.com/malikandemir