9.7 KiB
name, description, compatibility, metadata
| name | description | compatibility | metadata | ||||||
|---|---|---|---|---|---|---|---|---|---|
| home-k3s-deploy | Deploy and manage apps on junv's home K3s cluster at 192.168.1.2 (server-3). Use this skill when creating new Kubernetes manifests, adding services, configuring ingress/TLS, managing storage, or modifying existing deployments in this homelab cluster. Covers namespace conventions, storage patterns, ingress annotations, TLS, OAuth2 auth, node affinity, and apply workflow. | Requires kubectl configured for 192.168.1.2; manifests live in /Users/junv/code/home-docker |
|
Home K3s Deployment Skill
Cluster Overview
- Node:
server-3at192.168.1.2(single-node K3s cluster) - K3s version: v1.33.3+k3s1, Ubuntu 24.04.3
- SSH:
ssh -p 22422 junv@192.168.1.2(Ed25519 key) - Primary domain:
*.junv.cc(Cloudflare DNS + cert-manager Let's Encrypt) - Local network:
192.168.1.0/24(trusted, never block) - Kubeconfig:
scp -P 22422 junv@192.168.1.2:/etc/rancher/k3s/k3s.yaml ~/.kube/config && sed -i '' 's/127.0.0.1/192.168.1.2/g' ~/.kube/config
Namespace Organization
| Namespace | Purpose |
|---|---|
home-apps |
Home automation, productivity, personal tools |
media |
Media servers (alist, static files) |
db |
Databases: Redis (db ns), Qdrant vector DB |
ai |
AI/ML workloads |
argocd |
GitOps deployment (ArgoCD) |
ingress-nginx |
NGINX Ingress Controller |
cert-manager |
TLS certificate management |
crowdsec |
Security monitoring |
default |
Prometheus/Grafana monitoring |
Repository File Layout
New manifests go in the directory matching the app's namespace:
home-docker/
├── home-apps/ → namespace: home-apps
├── media/ → namespace: media
├── db/ → namespace: db
├── ai/ → namespace: ai
├── adhoc-config/ → one-off PVs, config patches
├── terraform/ → infra-as-code (prefer for cluster-level changes)
└── archive/ → deprecated configs (do not edit)
Adding a New App: Step-by-Step
- Choose the correct directory and namespace (see table above).
- Create a single YAML file
{app-name}.yamlwith all resources separated by---. - Resource order in the file: PV → PVC → Deployment → Service → Ingress.
- If local storage is needed, also create
adhoc-config/{app-name}-local-pv.yaml. - Apply:
kubectl apply -f {file}.yaml(orkubectl apply -f adhoc-config/{app-name}-local-pv.yamlfirst).
Storage Patterns
Local Storage (most common for new apps)
Use local-storage storageClass with node affinity pinned to server-3. Data path: /mnt/k8s/{app-name}/.
Create the directory first on the node: ssh -p 22422 junv@192.168.1.2 "sudo mkdir -p /mnt/k8s/{app-name}"
# adhoc-config/{app-name}-local-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: {app-name}-local-pv
labels:
type: local
name: {app-name}
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/k8s/{app-name}
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- server-3
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
namespace: {namespace}
name: {app-name}-local-pvc
spec:
volumeName: {app-name}-local-pv
storageClassName: "local-storage"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
NFS Storage (for large or shared data)
NFS server: 192.168.1.5, base path: /fs/1000/nfs/k8s/. Use storageClassName: "" (empty) with no volumeName binding.
apiVersion: v1
kind: PersistentVolume
metadata:
name: {app-name}-nfs-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
nfs:
server: 192.168.1.5
path: "/fs/1000/nfs/k8s/{app-name}"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
namespace: {namespace}
name: {app-name}-nfs-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: ""
resources:
requests:
storage: 10Gi
Deployment Pattern
Always set revisionHistoryLimit: 2. Pin the timezone. For images from Docker Hub that may be rate-limited, use mirror.gcr.io/ prefix (e.g., mirror.gcr.io/library/nginx:latest).
apiVersion: apps/v1
kind: Deployment
metadata:
name: {app-name}
namespace: {namespace}
labels:
app: {app-name}
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app: {app-name}
template:
metadata:
labels:
app: {app-name}
spec:
nodeSelector:
kubernetes.io/hostname: server-3
# If pulling from GitHub Container Registry:
imagePullSecrets:
- name: github-image-pull-secret
containers:
- name: {app-name}
image: mirror.gcr.io/{org}/{image}:{tag}
imagePullPolicy: IfNotPresent
ports:
- containerPort: {port}
env:
- name: TZ
value: Australia/Melbourne
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
volumes:
- name: data
persistentVolumeClaim:
claimName: {app-name}-local-pvc
Service Pattern
apiVersion: v1
kind: Service
metadata:
name: {app-name}
namespace: {namespace}
spec:
type: ClusterIP
selector:
app: {app-name}
ports:
- name: web
port: 80
targetPort: {container-port}
protocol: TCP
For direct LAN access (no ingress), use LoadBalancer type with a static MetalLB IP:
spec:
type: LoadBalancer
annotations:
metallb.universe.tf/loadBalancerIPs: 192.168.1.2XX
Ingress Patterns
Always use ingressClassName: nginx and cert-manager.io/cluster-issuer: "letsencrypt-prod" for TLS.
Public HTTPS (anyone on the internet)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {app-name}-ingress
namespace: {namespace}
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- {app-name}.junv.cc
secretName: {app-name}-tls
rules:
- host: {app-name}.junv.cc
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {app-name}
port:
number: 80
LAN-Only (IP whitelisted to home network)
Add the whitelist annotation — no OAuth needed:
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/whitelist-source-range: "192.168.1.0/24"
OAuth2-Protected (via Pocket ID / passkey-auth)
Add these two annotations to require login via pass.junv.cc:
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/auth-url: "https://pass.junv.cc/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://pass.junv.cc/oauth2/start?rd=https://$host$escaped_request_uri"
Existing Secrets (reference, do not recreate)
| Secret Name | Namespace | Keys | Purpose |
|---|---|---|---|
postgres-credential |
home-apps | username, password |
PostgreSQL at 192.168.1.2:5432 |
qdrant-api-key |
db | key |
Qdrant vector DB API key |
github-image-pull-secret |
home-apps | — | Pull from ghcr.io |
Use secrets via secretKeyRef:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credential
key: password
Infrastructure Services (in-cluster)
| Service | Address | Notes |
|---|---|---|
| PostgreSQL | 192.168.1.2:5432 |
Host-level install |
| Redis | 192.168.1.2:6379 |
Bitnami Helm chart, db namespace |
| Qdrant | qdrant.db.svc.cluster.local:6333 |
Vector DB, db namespace |
| ArgoCD | https://argo.junv.cc |
GitOps; add apps via ArgoCD UI or Application CR |
Common Conventions
- Timezone: Always set
TZ: Australia/Melbourne - Node pin:
nodeSelector: kubernetes.io/hostname: server-3for stateful workloads - Image mirror: Prefix Docker Hub images with
mirror.gcr.io/to avoid rate limits - Ingress class:
ingressClassName: nginx(not the legacy annotation) - TLS issuer:
letsencrypt-prodClusterIssuer (email: me@junv.cc) - Namespace on PVC/Ingress: Always specify
namespace:— it's easy to forget - Namespace on PV: PersistentVolumes are cluster-scoped — do NOT add
namespace: - revisionHistoryLimit: 2: Keep rollout history small
Apply Workflow
# Apply a new manifest
kubectl apply -f home-apps/{app-name}.yaml
# Apply a local-PV config first (before the main manifest)
kubectl apply -f adhoc-config/{app-name}-local-pv.yaml
kubectl apply -f home-apps/{app-name}.yaml
# Check status
kubectl get pods -n {namespace}
kubectl describe pod -n {namespace} {pod-name}
kubectl logs -n {namespace} -l app={app-name} --tail=50
# Restart a deployment
kubectl rollout restart deployment/{app-name} -n {namespace}
# Get events (useful for debugging PVC bind issues)
kubectl get events -n {namespace} --sort-by='.lastTimestamp'
Do NOT
- Delete files without user confirmation
- Use
iptablesintegration in CrowdSec (breaks kubectl access to port 6443) - Use inotify for container log acquisition in CrowdSec (use
poll_without_inotify: true) - Hardcode secrets in manifests — always use
secretKeyRef - Block
192.168.1.0/24— it's the trusted local network - Use SSH port 22 — the correct port is
22422