Homelab Infrastructure
A production-ready homelab and DevOps platform running on DigitalOcean Kubernetes with GitOps workflows and automated CI/CD pipelines.
🎯 5 Kubernetes Nodes
3x compute + 2x storage optimized nodes in Singapore
🔐 SSL Everywhere
Automatic Let's Encrypt certificates for all services
🔄 GitOps Managed
ArgoCD syncs all changes from Gitea automatically
🚀 CI/CD Pipeline
GitHub Actions → GHCR → Auto-deploy via ArgoCD
📊 9 Services
DevOps tools + homelab applications
☁️ Cloudflare DNS
Proxied DNS with DDoS protection
Platform Summary
| Component | Technology | Status |
|---|---|---|
| Cloud Provider | DigitalOcean (SGP1) | Active |
| Orchestrator | Kubernetes (DOKS) | Running |
| Ingress | NGINX Ingress Controller | Running |
| SSL | Cert-Manager + Let's Encrypt | Active |
| GitOps | ArgoCD | Synced |
| Source Control | Gitea (self-hosted) | Running |
| CI/CD | GitHub Actions + GHCR | Active |
| DNS | Cloudflare | Active |
Architecture
The platform follows a cloud-native architecture with GitOps at its core.
System Overview
CI/CD Pipeline
GitOps Flow
Cost Breakdown
Monthly infrastructure costs for the homelab platform.
| Resource | Specification | Monthly Cost |
|---|---|---|
| Basic Node Pool | 3x s-2vcpu-4gb ($12 each) | $36 |
| Storage Node Pool | 2x s-4vcpu-8gb ($24 each) | $48 |
| Load Balancer | NGINX Ingress LB | $12 |
| Block Storage | 100GB SSD (shared) | $10 |
| Total | ~$106/mo |
GitHub Container Registry (GHCR), GitHub Actions (2000 min/mo free), Let's Encrypt SSL, and Cloudflare DNS are all free tier.
Cost Optimization Tips
- Use fewer/smaller nodes during low-usage periods
- Enable DO's auto-scaling for dynamic workloads
- Monitor actual storage usage — reduce PVC sizes if over-provisioned
- Consider spot instances for non-critical workloads
- Consolidate services that don't need separate deployments
Cluster Setup
How the DOKS cluster is provisioned and configured.
Cluster Creation
# Create cluster with 2 node pools
doctl kubernetes cluster create homelab \
--region sgp1 \
--version latest \
--node-pool "name=basic-pool;size=s-2vcpu-4gb;count=3" \
--node-pool "name=storage-pool;size=s-4vcpu-8gb;count=2" \
--auto-upgrade --wait
# Save kubeconfig
doctl kubernetes cluster kubeconfig save homelab
# Verify
kubectl get nodes
Node Topology
Node Assignments
| Node Pool | Workloads | Reason |
|---|---|---|
| basic-pool (3x) | Core services, lightweight apps | Sufficient for most services |
| storage-pool (2x) | Jellyfin, Nextcloud, databases | More RAM + CPU for storage-heavy workloads |
Chosen for low latency to Southeast Asia users. Alternative regions: NYC1 (US East), SFO3 (US West), FRA1 (Europe).
Infrastructure Components
Core infrastructure that powers the entire platform.
NGINX Ingress Controller
Routes external traffic to internal services based on hostname. Installed via Helm.
- Creates a DigitalOcean Load Balancer automatically
- Handles TLS termination with Let's Encrypt certificates
- Routes traffic based on host headers (e.g.,
gitea.akze.net)
Cert-Manager + Let's Encrypt
Automatically provisions and renews SSL certificates for all services.
DigitalOcean Block Storage
SSD-backed persistent volumes for stateful services. Automatically provisioned by the DO CSI driver.
| Service | Storage | Purpose |
|---|---|---|
| Gitea | 10Gi | Git repositories |
| Grafana | 5Gi | Dashboards & data |
| Nextcloud | 50Gi | File storage |
| Jellyfin | 30Gi | Media + cache |
| Vaultwarden | 5Gi | Password database |
| Pi-hole | 2Gi | Block lists + logs |
| Home Assistant | 5Gi | Configuration |
| Uptime Kuma | 5Gi | Monitoring data |
DNS Configuration
All services are accessed via subdomains of akze.net, managed through Cloudflare.
DNS Records
| Subdomain | Type | Target | Proxy |
|---|---|---|---|
| *.akze.net (all services) | A | Load Balancer IP | OK Proxied |
argocd, gitea, grafana, kuma, jellyfin, nextcloud, vault, pihole, home, app, docs — all point to the same Load Balancer IP
Cloudflare-proxy domains are not resolvable from within the Kubernetes cluster. ArgoCD's repo-server uses hostAliases to map internal DNS to the Load Balancer IP directly.
All Services
Every service running on the platform, organized by category.
🔧 ArgoCD
GitOps continuous deployment — syncs Gitea manifests to cluster
Healthy📦 Gitea
Self-hosted Git server — stores all Kubernetes manifests
Healthy📊 Grafana
Metrics dashboards — monitors cluster and service health
Healthy💓 Uptime Kuma
Service monitoring — tracks availability and response times
Healthy🎬 Jellyfin
Media streaming — self-hosted Netflix alternative
Healthy☁️ Nextcloud
File sync & calendar — Google Drive replacement
Healthy🔐 Vaultwarden
Password manager — Bitwarden-compatible server
Healthy🛡️ Pi-hole
Network ad blocker — DNS-level ad filtering
Healthy🏠 Home Assistant
Smart home hub — IoT device management and automation
Healthy🚀 Sample App
CI/CD demo app — Go web app auto-deployed via pipeline
HealthyDevOps Tools
Professional DevOps tools for development and operations workflows.
ArgoCD — GitOps Engine
Continuously monitors the Gitea repository and automatically applies any manifest changes to the Kubernetes cluster. Supports auto-sync, self-healing, and rollback.
Gitea — Source Control
Lightweight, self-hosted Git server. Stores all Kubernetes manifests, Helm values, and deployment configurations. Acts as the single source of truth for the entire infrastructure.
Grafana — Observability
Dashboard platform for visualizing metrics. Connects to Prometheus data sources to display cluster resource usage, service health, and custom business metrics.
Uptime Kuma — Monitoring
Beautiful status page and uptime monitor. Checks all services periodically and sends alerts when services go down.
Homelab Applications
Personal productivity and entertainment applications.
Nextcloud
Complete self-hosted productivity suite with file sync, calendar, contacts, and collaborative editing. Uses Apache backend with 50Gi persistent storage.
Jellyfin
Free software media system. Streams movies, music, and TV shows to any device. Optimized with separate config and cache volumes (30Gi total).
Vaultwarden
Lightweight Bitwarden-compatible password manager. Stores all passwords encrypted, supports 2FA, and syncs across all devices.
Pi-hole
Network-level ad and tracker blocking. Works as a DNS sinkhole, blocking ads for all devices on the network without installing browser extensions.
Home Assistant
Open-source home automation platform. Connects to smart home devices, creates automations, and provides a unified dashboard for all IoT devices.
GitOps Workflow
Infrastructure as Code with automatic synchronization.
Repository Structure
homelab/
├── infrastructure/ # Core infrastructure
│ ├── nginx-ingress/ # NGINX Ingress Controller
│ ├── cert-manager/ # SSL certificates
│ └── storage/ # Block storage config
├── apps/ # Applications
│ ├── devops/ # DevOps services
│ │ ├── argocd/ # ArgoCD configuration
│ │ ├── gitea/ # Gitea deployment
│ │ ├── grafana/ # Grafana deployment
│ │ └── uptime-kuma/ # Uptime monitoring
│ └── homelab/ # Homelab services
│ ├── jellyfin/ # Media streaming
│ ├── nextcloud/ # File sync
│ ├── vaultwarden/ # Password manager
│ ├── pihole/ # Ad blocker
│ └── home-assistant/ # Smart home
└── scripts/ # Deployment scripts
├── 01-infrastructure.ps1
├── 02-devops.ps1
└── 03-homelab.ps1
Principles
- Declarative: Everything defined in YAML manifests
- Versioned: All changes tracked in Git history
- Automatic: No manual kubectl apply needed
- Self-Healing: ArgoCD auto-corrects drift
- Auditable: Every change has a commit message and author
CI/CD Pipeline
Automated build, test, and deployment pipeline for application code.
GitHub Actions Workflow
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- run: go test -v ./... # Run tests
- uses: docker/login-action@v3 # Login to GHCR
- uses: docker/build-push-action@v5 # Build & push
cd:
needs: ci
runs-on: ubuntu-latest
steps:
- run: |
git clone https://gitea.akze.net/user/homelab.git
# Update image tag in manifest
sed -i "s|image: .*|image: ghcr.io/user/app:${SHA}|" ...
git push # Push to Gitea
Why GHCR Over DO Registry?
| Feature | GHCR | DO Registry |
|---|---|---|
| Free Tier | Unlimited (public repos) | 1 repository |
| GitHub Integration | Native | Requires token |
| CI Authentication | Auto (GITHUB_TOKEN) | Manual setup |
| Image Pull in Kubernetes | PAT with read:packages | DO token |
| Cost | Free | Free (limited) |
All Docker builds happen on GitHub's runners. Developers only push code — no Docker installation needed on their machine.
Security Architecture
How the platform handles authentication, secrets, and access control.
Secret Management
| Secret Type | Storage | Scope |
|---|---|---|
| Kubernetes Secrets | etcd (encrypted at rest in DOKS) | Per-namespace |
| GitHub Actions Secrets | GitHub encrypted secrets | Per-repository |
| Gitea Access Tokens | GitHub Actions secrets + Kubernetes secrets | CI/CD pipeline |
| SSL Certificates | Kubernetes TLS Secrets (cert-manager managed) | Per-ingress |
Security Principles
🔒 No Secrets in Git
All tokens and passwords stored in Kubernetes secrets or CI/CD secrets — never in manifests
🛡️ Cloudflare Protection
All traffic proxied through Cloudflare — DDoS protection and WAF
🔐 TLS Everywhere
Every service has automatic Let's Encrypt SSL — no HTTP-only endpoints
👤 RBAC
Kubernetes RBAC controls access to cluster resources
🔑 Private Repos
Both Gitea and GitHub repos are private — no public code exposure
📋 Audit Trail
Every infrastructure change has a Git commit with author and timestamp
Network Security
Troubleshooting
Common issues and their solutions.
Service Returns 404
Ingress resources must have ingressClassName: nginx or NGINX controller won route them.
spec:
ingressClassName: nginx # Must be present
tls: ...
ArgoCD Redirect Loop
ArgoCD forces HTTPS internally + ingress also redirects → infinite loop.
# Fix: Set insecure mode
kubectl patch configmap argocd-cmd-params-cm -n argocd \
--type merge -p '{"data":{"server.insecure":"true"}}'
kubectl rollout restart deployment/argocd-server -n argocd
ImagePullBackOff
Kubernetes needs an imagePullSecret with a GitHub PAT that has read:packages scope.
# Create pull secret
kubectl create secret docker-registry ghcr-pull-secret \
-n <namespace> \
--docker-server=ghcr.io \
--docker-username=<github-user> \
--docker-password=ghp_YOUR_TOKEN
# Reference in deployment
# spec.template.spec.imagePullSecrets: [{name: ghcr-pull-secret}]
SSL Certificates Pending
Let's Encrypt needs to verify domain ownership via HTTP-01 challenge. Ensure port 80 is accessible and DNS points to the correct IP.
kubectl get certificates -A
kubectl describe certificate <name> -n <namespace>
kubectl logs deploy/cert-manager -n cert-manager
Common kubectl Commands
# Check all pods
kubectl get pods --all-namespaces
# Describe a failing pod
kubectl describe pod <name> -n <namespace>
# View logs
kubectl logs <pod> -n <namespace> --tail=50
# Restart a deployment
kubectl rollout restart deployment/<name> -n <namespace>
# Check ArgoCD applications
kubectl get applications -n argocd
# Force ArgoCD refresh
kubectl patch application <name> -n argocd \
--type merge -p '{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}'