Homelab 8 Nisan 2026

Homelab for everyone


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.

Minimum Requirements
🖥️
Any PC or laptop
Even old hardware works
💾
4 GB RAM minimum
8+ GB recommended
💿
100 GB storage
More = more media
🌐
Home internet
Any connection works
🔑
A domain name
~$10/year
☁️
Cloudflare account
Free forever

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:

How traffic flows to your server
VISITOR
Browser
DNS + CDN
Cloudflare
TUNNEL
cloudflared
ROUTER
Traefik
APP
Your Service
Cloudflare — hides your home IP, handles HTTPS, protects from attacks
cloudflared — connects your server to Cloudflare without opening ports
Traefik — reads the domain name and routes to the right app

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:

Containers vs Installing Directly
❌ WITHOUT DOCKER
App A needs Python 3.8
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
✅ WITH DOCKER
App A in its own box (Python 3.8)
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
⚠️ What each firewall rule does
deny incoming Block everything by default. Nothing gets in unless you explicitly allow it.
SSH LAN only You can SSH from home but nobody on the internet can even see port 22.
80 + 443 open Traefik listens here. It handles all web traffic and routes to the right app.

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
Directory layout explained
/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

  1. Sign up at cloudflare.com (free)
  2. Add your domain → follow the steps to update your nameservers
  3. Wait 5-10 minutes for DNS to propagate

5b — Create a Tunnel

  1. Cloudflare dashboard → Zero TrustNetworksTunnels
  2. Create a tunnel → name it my-homelab
  3. Copy the tunnel token (long string starting with eyJ...)

5c — Add DNS records

In Cloudflare DNS, add these records pointing to your public IP:

TypeNameContentProxy
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.me on 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.

How Traefik routing works
Request for photos.yourdomain.com → Traefik reads Host header → routes to Immich container
Request for yourdomain.com → Traefik reads Host header → routes to Website container
Request for ha.yourdomain.com → Traefik reads Host header → routes to Home Assistant
One server. Many domains. Zero port conflicts. All managed automatically.

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:

The 4-label pattern — memorize this
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
Replace APPNAME with a unique name, sub.domain.com with your subdomain, 3000 with your app's 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

SubdomainDomainTypeURL
(empty)yourdomain.comHTTP192.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:

Services running on my home lab
📷
Immich
Self-hosted Google Photos. Automatic phone backup, face recognition, smart albums.
photos.yourdomain.com
🏠
Home Assistant
Controls all smart home devices. Automations, dashboards, integrations.
ha.yourdomain.com
☁️
Nextcloud
Self-hosted Dropbox. Files, calendar, contacts, notes — your data, your server.
cloud.yourdomain.com
🎬
Jellyfin
Self-hosted Netflix. Stream your movies and music on any device.
jellyfin.yourdomain.com
n8n
Visual automation workflows. Connect all your services together without code.
n8n.yourdomain.com

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:

CI/CD pipeline
git push
GitHub Actions triggers
SSH via Cloudflare Tunnel
docker compose up -d
Live in seconds

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:

SecretValue
SSH_PRIVATE_KEYContents of ~/.ssh/github_actions
SSH_HOSTssh.yourdomain.com
SSH_USERyour 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

Things that will go wrong (and how to fix them)
502 Bad Gateway
Container isn't running or not on traefik-public network. Run: docker ps | grep appname
404 Not Found from Traefik
Traefik doesn't see your container. Check labels and make sure you have a single service: APPNAME-svc for both routers
Certificate not issuing
Check your Cloudflare API token has DNS edit permissions. Check docker logs traefik | grep acme
Container unhealthy, Traefik drops it
Some apps have broken health checks. Add healthcheck: disable: true to the compose file
Port forwarding doesn't work
You're probably behind CGNAT (common with Turkish and mobile ISPs). Use Cloudflare Tunnel — it bypasses this completely

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