This repo contains prebuilt Kubernetes “stack” manifests for a homelab.
A stack here means: one YAML file you apply, and Kubernetes creates everything needed for a related set of apps.
Each stack file is a single multi-document YAML (multiple Kubernetes objects separated by ---). When applied, it typically creates:
- a dedicated Namespace (a logical folder for the stack)
- app workloads (usually Deployments or StatefulSets)
- Services (so you can reach the apps)
- storage (either PVCs or direct volume mounts)
These manifests are intended to be applied as-is first, then customized to match your environment.
Apply a stack:
kubectl apply -f k8s/lab.yaml
kubectl apply -f k8s/media.yamlCheck that resources came up:
kubectl get ns
kubectl -n lab-stack get all
kubectl -n media-stack get allRemove a stack:
kubectl delete -f k8s/lab.yaml
kubectl delete -f k8s/media.yamlMost apps are exposed using NodePort Services.
That means you can access an app at:
http://<node-ip>:<nodePort>
To see the configured ports:
kubectl -n lab-stack get svc
kubectl -n media-stack get svcThis repo uses two common storage patterns:
Some apps request storage using PVCs, often referencing a StorageClass (for example, nfs).
What you need: the referenced StorageClass must already exist in your cluster.
Some deployments mount volumes via nfs directly, and they are currently tuned for my homelab:
- NFS server:
10.1.2.5
If you clone/fork this repo, you will almost certainly need to update server: 10.1.2.5 to match your NAS/NFS server (or switch to PVCs backed by your own StorageClass).
If the NFS server address is wrong or unreachable, pods will usually get stuck starting with mount-related errors.
Some workloads expect Kubernetes Secrets (usually passwords) to exist before you apply the stack.
General pattern (examples):
kubectl -n lab-stack create secret generic mysql --from-literal=MYSQL_ROOT_PASSWORD="<your-mysql-root-password>"
kubectl -n lab-stack create secret generic npm --from-literal=MYSQL_NPM_PASSWORD="<your-npm-password>"
kubectl -n lab-stack create secret generic mongodb --from-literal=MONGO_INITDB_ROOT_PASSWORD="<your-mongodb-root-password>"
kubectl -n lab-stack create secret generic influxdb --from-literal=DOCKER_INFLUXDB_INIT_PASSWORD="<your-influxdb-password>"
kubectl -n lab-stack create secret generic teamcity --from-literal=TEAMCITY_DB_PASSWORD="<your-teamcity-db-password>"
kubectl -n lab-stack create secret generic pihole --from-literal=PIHOLE_PASSWORD="<your-pihole-password>"If a required secret is missing, the related pods will typically fail to start. Troubleshoot with:
kubectl -n describe pod
kubectl -n get events --sort-by=.lastTimestampA general-purpose “lab” environment: databases, platform tools, monitoring, and utilities.
Creates (high level):
- Namespace:
lab-stack - Storage: multiple PVCs for apps that need persistence
- Datastores: MySQL, Redis, MongoDB (replica set + init job)
- Platform apps: nginx, SearXNG, Open WebUI, n8n, TeamCity, Nginx Proxy Manager
- Monitoring: Grafana, InfluxDB, Prometheus
- Utilities/exporters: Pi-hole exporters
Media management and downloading tools.
Creates (high level):
- Namespace:
media-stack - Apps: Prowlarr, Lidarr, Radarr, Sonarr, Sabnzbd, Transmission
- Networking: each app exposed via NodePort
- Storage: app configs and media/download paths are mounted for persistence (see the NFS note above)
This repo includes Docker Compose equivalents of the Kubernetes stacks, useful if you don't have a Kubernetes cluster.
The Docker Compose stacks use a shared network (gotham-net) between lab and media so that OpenClaw can reach SearXNG, Lidarr, Radarr, and Sonarr by their container hostnames. Both stacks must be running on the same Docker host.
Both stacks use bind mounts that map to /mnt/docker/*. You'll need the NFS share mounted on the Docker host (e.g. 10.1.2.5:/docker /mnt/docker nfs).
A general-purpose lab environment (same apps as lab.yaml but without Kubernetes).
cd docker/lab
docker compose up -dStops the lab stack:
cd docker/lab
docker compose downMedia management and downloading tools (same apps as media.yaml but without Kubernetes).
cd docker/media
docker compose up -dStops the media stack:
cd docker/media
docker compose downYou can bring up either stack independently. The shared gotham-net network is created automatically when the first service joins it. For clarity:
# Bring up lab (creates gotham-net)
cd docker/lab && docker compose up -d
# Bring up media (joins existing gotham-net)
cd ../media && docker compose up -dOr the other way around — order doesn't matter since Docker creates the network on first use.
All apps are exposed directly on the host at http://<host-ip>:<port>. Run docker compose ps to see the port mappings for each service.
Volume paths use /mnt/* instead of NFS-backed PVCs. See each docker-compose.yaml for details.
- NodePort: great for homelabs; access apps via
http://<node-ip>:<nodePort>. - Timezone / IDs: some containers use
TZ,PUID, andPGID. Adjust to match your environment. - Cluster DNS differences: some pods set explicit DNS settings; if your cluster setup differs, you may need to adjust those.
- More portability: if you want easier migration across clusters/nodes, prefer PVCs backed by shared storage over node-specific paths.
These manifests were designed around a specific homelab setup (so expect to tweak things):
- CPU: AMD Epyc 4464P (12c/24t)
- RAM: 128 GiB
- Host OS: Proxmox 9
- Network: bonded 25 GbE (50 GbE)
- OS: Ubuntu Server 24.04 LTS (microk8s)
- CPU: 8 threads
- RAM: 24 GiB
- Disk: 256 GiB
Node IPs:
10.1.2.5010.1.2.5110.1.2.52
Local DNS uses kube.lan plus per-node hostnames like kube-N.lan.
10.1.2.210.1.2.3
10.1.2.5