Self-Hosted CI/CD in a Humble Homelab: Practical, Durable DevOps at Home
If you’re reading this, you’ve likely burned enough hours chasing flaky CI runners in the cloud to know there’s real value in hosting your own pipelines at home. This isn't about chasing perfection in the cloud; it’s about getting predictable builds, private code, and learn-by-doing automation on hardware you control. In my homelab, I run a lean CI/CD workflow that keeps me honest: reproducible builds, quick feedback, and a clean boundary between build/test tooling and personal data. Here’s a down-to-earth blueprint you can copy, adapt, and actually use.
Why bother self-hosting in a homelab?
- Privacy and control: Your source, secrets, and artifacts stay on gear you own. No surprise access via a third party.
- Cost predictability: Once you’ve amortized hardware, the ongoing cost is mostly power, network, and maintenance.
- Learn-by-doing: You’ll fix real production issues (build cache, artifacts, runners, secret handling) in a safe, low-stakes environment.
- Faster feedback in some cases: A well-tuned home CI can be snappy, especially for small to medium repos, and you can optimize for your pipeline exactly.
What you should aim for
- An observable, simple stack you can maintain: a git hosting tool, a CI engine, a runner, and a reverse proxy with TLS.
- Separation of concerns: git hosting is separate from CI; CI runners are separate from the internet entrypoint; backups exist for data and configs.
- Secure defaults: least privilege, rotate secrets, encrypted traffic, offline backups.
The minimal practical stack I recommend
- Git hosting: Gitea (lightweight, easy to run, sufficient for solo and small teams).
- CI/CD: Drone CI (lightweight, good Git integration, simple to run with a runner).
- Runner: Docker runner (Drone can use Docker-exec style runners; this keeps builds isolated).
- Ingress and TLS: Traefik (dynamic routing, Let’s Encrypt integration).
- Persistence: local volumes for databases and caches; backups on a separate NAS or USB drive.
- Optional monitoring: Prometheus + Grafana (minimal dashboards to observe builds, runner status, disk I/O).
If you want a heavier but more “all-in-one” approach later, you can swap in GitLab CE instead of Gitea + Drone. For now, the lighter combo is easier to spin up, more forgiving in a home network, and still very practical for automation work.
Architecture sketch
- Internet -> Traefik (TLS, routing to internal services)
- Traefik routes to Gitea for git hosting and to Drone Server for CI
- Drone Server talks to Drone Runner to execute jobs
- Logs and artifacts stored in dedicated volumes
- Optional: a small separate VM or container for backups and a minimal monitoring stack
Prerequisites and ground rules
- Hardware: A modest but reliable box (e.g., 4 cores, 16 GB RAM minimum for a starter; 8+ cores and 32 GB for smoother operation with multiple repos and larger pipelines). An SSD for faster I/O helps.
- Networking: A fixed internal network, a static or reserved IP for your homelab, and a domain/subdomain you control (e.g., ci.yourdomain.local or home.lab). If you want TLS, you’ll need outbound DNS to Let's Encrypt (or provide a valid DNS for your domain).
- OS and tooling: A fresh Debian/Ubuntu or similar Linux, Docker Engine, docker-compose, and a basic firewall ( ufw or nftables ). I typically run everything in Docker Compose for ease of maintenance.
- Backups: Regular backups of Gitea data, Drone data, and Traefik configuration. Store backups to a separate disk or a NAS on the network.
Step-by-step plan: get a lean CI/CD stack running
Step 1: Prepare the host and install Docker
- Install your OS and update it.
- Install Docker and docker-compose.
- curl -fsSL https://get.docker.com -o get-docker.sh
- sh get-docker.sh
- sudo usermod -aG docker $USER
- sudo apt-get install -y docker-compose
- Create a dedicated directory for your stack, with subdirs for data and configs:
- mkdir -p ~/homelab/ci/{gitea,drone,traefik,shared}
- chown -R $USER:$USER ~/homelab
Step 2: Traefik as the entrance
The goal is one public entry point with TLS, and internal routing to Gitea and Drone.
- Create a docker-compose.yml in ~/homelab/ci:
- version: "3.8"
- services:
- traefik:
image: traefik:v2.9
command:
- --providers.docker
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesResolvers.myresolver.acme.httpChallenge.entryPoint=web
- --certificatesResolvers.myresolver.acme.email=you@example.com
- --certificatesResolvers.myresolver.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ~/homelab/ci/traefik/acme.json:/letsencrypt/acme.json
networks:
- web
restart: unless-stopped
- gitea:
image: gitea/gitea:latest
environment:
- USER_UID=1000
- USER_GID=1000
- DISABLE_SSH=true
volumes:
- ~/homelab/ci/shared/gitea:/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitea.rule=Host(
gitea.yourdomain.local)" - "traefik.http.routers.gitea.entrypoints=websecure"
- "traefik.http.routers.gitea.tls=true"
- "traefik.http.routers.gitea.tls.certresolver=myresolver"
networks:
- web
depends_on:
- postgres
- postgres:
image: postgres:13-alpine
environment:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=changeme
- POSTGRES_DB=gitea
volumes:
- ~/homelab/ci/shared/post