commit e07a5e1dfafa5dd09f1c54cbab737c088fca3b1f Author: Alvin Wang Date: Sun Apr 19 19:22:22 2026 -0400 Initial commit: k3s homelab infrastructure Helm charts for media stack (Plex, Sonarr, Radarr, etc.), dashboards (Glance, Homepage), paperless-ngx, mealie, traefik ingress, MetalLB, and utilities. Includes SOPS-encrypted secrets and bootstrap script. diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..2c457cf --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,6 @@ +creation_rules: + - path_regex: secrets/.*\.yaml$ + age: >- + age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te + # Replace the above with your actual age public key from: + # grep 'public key' /etc/sops/age/keys.txt diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..09fc036 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,6 @@ +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +dogbox Ready control-plane 3h31m v1.34.6+k3s1 10.0.1.2 Fedora Linux 40 (Server Edition) 6.9.6-200.fc40.x86_64 containerd://2.2.2-bd1.34 +mac-worker Ready 3h13m v1.34.6+k3s1 192.168.139.12 Ubuntu 25.10 6.17.8-orbstack-00308-g8f9c941121b1 containerd://2.2.2-bd1.34 + + +The mac-worker is running inside orbstack linux VM if that matters. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1df33f --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# Homelab — k3s Cluster + +2-node k3s cluster (1 manager, 1 worker) running a self-hosted homelab stack. + +## Architecture + +``` +Manager Node Worker Node +┌──────────────┐ ┌──────────────┐ +│ k3s server │────────────│ k3s agent │ +│ Traefik │ │ │ +│ Longhorn │ │ Longhorn │ +└──────┬───────┘ └──────┬───────┘ + │ │ + └───────── NFS ─────────────┘ + /dogstore +``` + +**Storage strategy:** + +- `/dogstore` (NFS) — mounted on both nodes, used via hostPath for large media/data volumes +- Longhorn — replicated block storage for small config/state volumes + +**Ingress:** k3s built-in Traefik with Let's Encrypt via Cloudflare DNS challenge. + +**Secrets:** SOPS + age encryption. Secrets live in `secrets/secrets.enc.yaml`, encrypted at rest. + +## Services + + +| Chart | Namespace | Services | +| -------------- | ----------- | -------------------------------------------------------------- | +| traefik-config | kube-system | Traefik HelmChartConfig overlay | +| media | media | Plex, Sonarr, Radarr, Bazarr, Prowlarr, qBittorrent, unpackerr | +| paperless | paperless | Paperless-ngx, Redis, PostgreSQL | +| mealie | apps | Mealie | +| dashboards | apps | Homepage, Glance | +| utils | apps | Zerobyte, Seerr | + + +## Prerequisites + +- Two Linux machines with NFS `/dogstore` mounted on both +- `curl`, `helm`, `kubectl`, `sops`, `age` installed + +## Bootstrap + +### 1. Install k3s server (manager node) + +```bash +./scripts/bootstrap.sh server +``` + +This prints the worker join command at the end. + +### 2. Install k3s agent (worker node) + +```bash +K3S_URL="https://:6443" K3S_TOKEN="" ./scripts/bootstrap.sh agent +``` + +### 3. Install Longhorn + +```bash +./scripts/bootstrap.sh longhorn +``` + +### 4. Set up SOPS encryption + +Generate an age keypair (run on each node): + +```bash +./scripts/bootstrap.sh sops-keygen +``` + +Copy the public key into `.sops.yaml`, replacing the placeholder. Then encrypt your secrets: + +```bash +# Edit secrets/secrets.enc.yaml — replace REPLACE_WITH_* placeholders with real values +sops -e -i secrets/secrets.enc.yaml +``` + +### 5. Apply secrets + +```bash +./scripts/bootstrap.sh apply-secrets +``` + +### 6. Deploy all charts + +```bash +./scripts/bootstrap.sh deploy +``` + +Or deploy individually: + +```bash +kubectl create namespace media +helm upgrade --install media charts/media -n media + +kubectl create namespace paperless +helm upgrade --install paperless charts/paperless -n paperless + +kubectl create namespace apps +helm upgrade --install mealie charts/mealie -n apps +helm upgrade --install dashboards charts/dashboards -n apps +helm upgrade --install utils charts/utils -n apps + +# Traefik config goes in kube-system (managed by k3s) +helm upgrade --install traefik-config charts/traefik-config -n kube-system +``` + +## Verifying + +```bash +# Check all pods +kubectl get pods -A + +# Check ingress routes +kubectl get ingress -A + +# Test a specific service +curl -I https://mealie.ratboo.me +``` + +## Secret Rotation + +1. Decrypt: `sops secrets/secrets.enc.yaml` (opens in `$EDITOR`) +2. Change the values +3. Save and close (SOPS re-encrypts automatically) +4. Apply: `./scripts/bootstrap.sh apply-secrets` +5. Restart affected pods: `kubectl rollout restart deployment/ -n ` + +## Repo Structure + +``` +homelab/ +├── README.md +├── .sops.yaml +├── scripts/ +│ └── bootstrap.sh +├── charts/ +│ ├── traefik-config/ +│ ├── media/ +│ ├── paperless/ +│ ├── mealie/ +│ ├── dashboards/ +│ └── utils/ +└── secrets/ + └── secrets.enc.yaml +``` + diff --git a/charts/dashboards/Chart.yaml b/charts/dashboards/Chart.yaml new file mode 100644 index 0000000..f698570 --- /dev/null +++ b/charts/dashboards/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: dashboards +description: Homepage and Glance dashboard services +version: 0.1.0 +type: application diff --git a/charts/dashboards/templates/glance-ingressroute.yaml b/charts/dashboards/templates/glance-ingressroute.yaml new file mode 100644 index 0000000..6cfc122 --- /dev/null +++ b/charts/dashboards/templates/glance-ingressroute.yaml @@ -0,0 +1,15 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: glance + annotations: + kubernetes.io/ingress.class: traefik-internal +spec: + entryPoints: + - web + routes: + - match: PathPrefix(`/`) + kind: Rule + services: + - name: glance + port: 8082 diff --git a/charts/dashboards/templates/glance.yaml b/charts/dashboards/templates/glance.yaml new file mode 100644 index 0000000..aa8cd38 --- /dev/null +++ b/charts/dashboards/templates/glance.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: glance + labels: + app: glance +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: glance + template: + metadata: + labels: + app: glance + spec: + nodeSelector: + node-role.kubernetes.io/control-plane: "true" + tolerations: + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + containers: + - name: glance + image: {{ .Values.glance.image }} + ports: + - containerPort: 8080 + env: + - name: DOMAIN + value: {{ .Values.domain }} + - name: HOST_IP + value: {{ .Values.hostIp | quote }} + - name: ADGUARD_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.glance.secretName }} + key: ADGUARD_PASSWORD + volumeMounts: + - name: config + mountPath: /app/config + volumes: + - name: config + hostPath: + path: /dogstore/service-data/glance/config + type: Directory +--- +apiVersion: v1 +kind: Service +metadata: + name: glance +spec: + selector: + app: glance + ports: + - port: 8082 + targetPort: 8080 diff --git a/charts/dashboards/templates/homepage.yaml b/charts/dashboards/templates/homepage.yaml new file mode 100644 index 0000000..c83ef55 --- /dev/null +++ b/charts/dashboards/templates/homepage.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: homepage + labels: + app: homepage +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: homepage + template: + metadata: + labels: + app: homepage + spec: + nodeSelector: + node-role.kubernetes.io/control-plane: "true" + tolerations: + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + containers: + - name: homepage + image: {{ .Values.homepage.image }} + ports: + - containerPort: 3000 + env: + - name: HOMEPAGE_ALLOWED_HOSTS + value: "*" + volumeMounts: + - name: config + mountPath: /app/config + volumes: + - name: config + hostPath: + path: /dogstore/service-data/homepage/config + type: Directory +--- +apiVersion: v1 +kind: Service +metadata: + name: homepage +spec: + selector: + app: homepage + ports: + - port: 3000 + targetPort: 3000 diff --git a/charts/dashboards/values.yaml b/charts/dashboards/values.yaml new file mode 100644 index 0000000..e45c4c4 --- /dev/null +++ b/charts/dashboards/values.yaml @@ -0,0 +1,10 @@ +domain: ratboo.me +hostIp: "10.0.1.2" + + +homepage: + image: ghcr.io/gethomepage/homepage:latest + +glance: + image: glanceapp/glance + secretName: glance-secrets diff --git a/charts/mealie/Chart.yaml b/charts/mealie/Chart.yaml new file mode 100644 index 0000000..7a74653 --- /dev/null +++ b/charts/mealie/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: mealie +description: Mealie recipe manager +version: 0.1.0 +type: application diff --git a/charts/mealie/templates/mealie.yaml b/charts/mealie/templates/mealie.yaml new file mode 100644 index 0000000..8533eb1 --- /dev/null +++ b/charts/mealie/templates/mealie.yaml @@ -0,0 +1,101 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mealie-data + labels: + app: mealie +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.storageSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mealie + labels: + app: mealie +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: mealie + template: + metadata: + labels: + app: mealie + spec: + containers: + - name: mealie + image: {{ .Values.image }} + ports: + - containerPort: 9000 + resources: + limits: + memory: {{ .Values.resources.limits.memory }} + env: + - name: PUID + value: {{ .Values.puid | quote }} + - name: PGID + value: {{ .Values.pgid | quote }} + - name: TZ + value: {{ .Values.tz | quote }} + - name: MAX_WORKERS + value: "1" + - name: WEB_CONCURRENCY + value: "1" + - name: ALLOW_SIGNUP + value: "false" + - name: BASE_URL + value: https://mealie.{{ .Values.domain }} + - name: OPENAI_BASE_URL + value: {{ .Values.ai.baseUrl }} + - name: OPENAI_MODEL + value: {{ .Values.ai.model }} + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.secretName }} + key: OPENAI_API_KEY + volumeMounts: + - name: data + mountPath: /app/data + volumes: + - name: data + persistentVolumeClaim: + claimName: mealie-data +--- +apiVersion: v1 +kind: Service +metadata: + name: mealie +spec: + selector: + app: mealie + ports: + - port: 9000 + targetPort: 9000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: mealie + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }} +spec: + rules: + - host: mealie.{{ .Values.domain }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: mealie + port: + number: 9000 diff --git a/charts/mealie/values.yaml b/charts/mealie/values.yaml new file mode 100644 index 0000000..6d5cd2d --- /dev/null +++ b/charts/mealie/values.yaml @@ -0,0 +1,19 @@ +domain: ratboo.me +certResolver: myresolver +tz: America/Los_Angeles +puid: "1000" +pgid: "1000" + +image: ghcr.io/mealie-recipes/mealie:v3.14.0 + +secretName: mealie-secrets +storageClass: longhorn +storageSize: 5Gi + +resources: + limits: + memory: 1000Mi + +ai: + baseUrl: https://generativelanguage.googleapis.com/v1beta + model: gemini-2.0-flash diff --git a/charts/media/Chart.yaml b/charts/media/Chart.yaml new file mode 100644 index 0000000..16620d3 --- /dev/null +++ b/charts/media/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: media +description: Media stack -- Plex, Sonarr, Radarr, Bazarr, Prowlarr, qBittorrent, unpackerr +version: 0.1.0 +type: application diff --git a/charts/media/templates/_helpers.tpl b/charts/media/templates/_helpers.tpl new file mode 100644 index 0000000..3127f2c --- /dev/null +++ b/charts/media/templates/_helpers.tpl @@ -0,0 +1,13 @@ +{{- define "media.labels" -}} +app.kubernetes.io/managed-by: Helm +app.kubernetes.io/part-of: media +{{- end -}} + +{{- define "media.commonEnv" -}} +- name: PUID + value: {{ .Values.puid | quote }} +- name: PGID + value: {{ .Values.pgid | quote }} +- name: TZ + value: {{ .Values.tz | quote }} +{{- end -}} diff --git a/charts/media/templates/bazarr.yaml b/charts/media/templates/bazarr.yaml new file mode 100644 index 0000000..3a7dd52 --- /dev/null +++ b/charts/media/templates/bazarr.yaml @@ -0,0 +1,72 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: bazarr-config + labels: + app: bazarr + {{- include "media.labels" . | nindent 4 }} +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.bazarr.configSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bazarr + labels: + app: bazarr + {{- include "media.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: bazarr + template: + metadata: + labels: + app: bazarr + spec: + containers: + - name: bazarr + image: {{ .Values.bazarr.image }} + ports: + - containerPort: 6767 + env: + {{- include "media.commonEnv" . | nindent 12 }} + volumeMounts: + - name: config + mountPath: /config + - name: movies + mountPath: /movies + - name: tv + mountPath: /tv + volumes: + - name: config + persistentVolumeClaim: + claimName: bazarr-config + - name: movies + hostPath: + path: {{ .Values.dogstore }}/sonarr/data/radarr-library + type: DirectoryOrCreate + - name: tv + hostPath: + path: {{ .Values.dogstore }}/sonarr/data/library + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: bazarr + labels: + app: bazarr +spec: + selector: + app: bazarr + ports: + - port: 6767 + targetPort: 6767 diff --git a/charts/media/templates/plex.yaml b/charts/media/templates/plex.yaml new file mode 100644 index 0000000..dfd6648 --- /dev/null +++ b/charts/media/templates/plex.yaml @@ -0,0 +1,108 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: plex-config + labels: + app: plex + {{- include "media.labels" . | nindent 4 }} +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.plex.configSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: plex + labels: + app: plex + {{- include "media.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: plex + template: + metadata: + labels: + app: plex + spec: + containers: + - name: plex + image: {{ .Values.plex.image }} + ports: + - containerPort: 32400 + protocol: TCP + env: + {{- include "media.commonEnv" . | nindent 12 }} + - name: PLEX_CLAIM + valueFrom: + secretKeyRef: + name: {{ .Values.secretName }} + key: PLEX_CLAIM + - name: ADVERTISE_IP + value: {{ .Values.plex.advertiseIp | quote }} + - name: VERSION + value: docker + - name: NVIDIA_VISIBLE_DEVICES + value: all + - name: NVIDIA_DRIVER_CAPABILITIES + value: compute,video,utility + volumeMounts: + - name: config + mountPath: /config + - name: transcode + mountPath: /transcode + - name: data + mountPath: /data + volumes: + - name: config + persistentVolumeClaim: + claimName: plex-config + - name: transcode + hostPath: + path: {{ .Values.dogstore }}/plex/transcode + type: DirectoryOrCreate + - name: data + hostPath: + path: {{ .Values.dogstore }} + type: Directory +--- +apiVersion: v1 +kind: Service +metadata: + name: plex + labels: + app: plex +spec: + selector: + app: plex + ports: + - name: web + port: 32400 + targetPort: 32400 + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: plex + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }} +spec: + rules: + - host: plex.{{ .Values.domain }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: plex + port: + number: 32400 diff --git a/charts/media/templates/prowlarr.yaml b/charts/media/templates/prowlarr.yaml new file mode 100644 index 0000000..9f20263 --- /dev/null +++ b/charts/media/templates/prowlarr.yaml @@ -0,0 +1,60 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: prowlarr-config + labels: + app: prowlarr + {{- include "media.labels" . | nindent 4 }} +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.prowlarr.configSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prowlarr + labels: + app: prowlarr + {{- include "media.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: prowlarr + template: + metadata: + labels: + app: prowlarr + spec: + containers: + - name: prowlarr + image: {{ .Values.prowlarr.image }} + ports: + - containerPort: 9696 + env: + {{- include "media.commonEnv" . | nindent 12 }} + volumeMounts: + - name: config + mountPath: /config + volumes: + - name: config + persistentVolumeClaim: + claimName: prowlarr-config +--- +apiVersion: v1 +kind: Service +metadata: + name: prowlarr + labels: + app: prowlarr +spec: + selector: + app: prowlarr + ports: + - port: 9696 + targetPort: 9696 diff --git a/charts/media/templates/qbittorrent.yaml b/charts/media/templates/qbittorrent.yaml new file mode 100644 index 0000000..23f5e74 --- /dev/null +++ b/charts/media/templates/qbittorrent.yaml @@ -0,0 +1,82 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: qbittorrent-config + labels: + app: qbittorrent + {{- include "media.labels" . | nindent 4 }} +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.qbittorrent.configSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: qbittorrent + labels: + app: qbittorrent + {{- include "media.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: qbittorrent + template: + metadata: + labels: + app: qbittorrent + spec: + containers: + - name: qbittorrent + image: {{ .Values.qbittorrent.image }} + ports: + - containerPort: {{ .Values.qbittorrent.webuiPort }} + protocol: TCP + - containerPort: 34034 + protocol: TCP + - containerPort: 34034 + protocol: UDP + env: + {{- include "media.commonEnv" . | nindent 12 }} + - name: WEBUI_PORTS + value: "{{ .Values.qbittorrent.webuiPort }}/tcp" + volumeMounts: + - name: config + mountPath: /config + - name: data + mountPath: /data + volumes: + - name: config + persistentVolumeClaim: + claimName: qbittorrent-config + - name: data + hostPath: + path: {{ .Values.dogstore }}/sonarr/data + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: qbittorrent + labels: + app: qbittorrent +spec: + selector: + app: qbittorrent + ports: + - name: webui + port: {{ .Values.qbittorrent.webuiPort }} + targetPort: {{ .Values.qbittorrent.webuiPort }} + - name: bt-tcp + port: 34034 + targetPort: 34034 + protocol: TCP + - name: bt-udp + port: 34034 + targetPort: 34034 + protocol: UDP diff --git a/charts/media/templates/radarr.yaml b/charts/media/templates/radarr.yaml new file mode 100644 index 0000000..b4a7bc5 --- /dev/null +++ b/charts/media/templates/radarr.yaml @@ -0,0 +1,86 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: radarr-config + labels: + app: radarr + {{- include "media.labels" . | nindent 4 }} +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.radarr.configSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: radarr + labels: + app: radarr + {{- include "media.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: radarr + template: + metadata: + labels: + app: radarr + spec: + containers: + - name: radarr + image: {{ .Values.radarr.image }} + ports: + - containerPort: 7878 + env: + {{- include "media.commonEnv" . | nindent 12 }} + volumeMounts: + - name: config + mountPath: /config + - name: data + mountPath: /data + volumes: + - name: config + persistentVolumeClaim: + claimName: radarr-config + - name: data + hostPath: + path: {{ .Values.dogstore }}/sonarr/data + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: radarr + labels: + app: radarr +spec: + selector: + app: radarr + ports: + - port: 7878 + targetPort: 7878 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: radarr + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }} +spec: + rules: + - host: radarr.{{ .Values.domain }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: radarr + port: + number: 7878 diff --git a/charts/media/templates/sonarr.yaml b/charts/media/templates/sonarr.yaml new file mode 100644 index 0000000..beda81c --- /dev/null +++ b/charts/media/templates/sonarr.yaml @@ -0,0 +1,86 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sonarr-config + labels: + app: sonarr + {{- include "media.labels" . | nindent 4 }} +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.sonarr.configSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sonarr + labels: + app: sonarr + {{- include "media.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: sonarr + template: + metadata: + labels: + app: sonarr + spec: + containers: + - name: sonarr + image: {{ .Values.sonarr.image }} + ports: + - containerPort: 8989 + env: + {{- include "media.commonEnv" . | nindent 12 }} + volumeMounts: + - name: config + mountPath: /config + - name: data + mountPath: /data + volumes: + - name: config + persistentVolumeClaim: + claimName: sonarr-config + - name: data + hostPath: + path: {{ .Values.dogstore }}/sonarr/data + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: sonarr + labels: + app: sonarr +spec: + selector: + app: sonarr + ports: + - port: 8989 + targetPort: 8989 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: sonarr + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }} +spec: + rules: + - host: sonarr.{{ .Values.domain }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: sonarr + port: + number: 8989 diff --git a/charts/media/templates/unpackerr.yaml b/charts/media/templates/unpackerr.yaml new file mode 100644 index 0000000..87d6005 --- /dev/null +++ b/charts/media/templates/unpackerr.yaml @@ -0,0 +1,93 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: unpackerr + labels: + app: unpackerr + {{- include "media.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: unpackerr + template: + metadata: + labels: + app: unpackerr + spec: + containers: + - name: unpackerr + image: {{ .Values.unpackerr.image }} + env: + - name: UN_DEBUG + value: "false" + - name: UN_LOG_FILE + value: /logs/log.txt + - name: UN_LOG_FILES + value: "10" + - name: UN_LOG_FILE_MB + value: "10" + - name: UN_INTERVAL + value: 1m + - name: UN_START_DELAY + value: 1m + - name: UN_RETRY_DELAY + value: 5m + - name: UN_MAX_RETRIES + value: "3" + - name: UN_PARALLEL + value: "1" + - name: UN_FILE_MODE + value: "0644" + - name: UN_DIR_MODE + value: "0755" + - name: UN_SONARR_0_URL + value: http://sonarr:8989 + - name: UN_SONARR_0_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.secretName }} + key: SONARR_API_KEY + - name: UN_SONARR_0_PATHS_0 + value: /data/library + - name: UN_SONARR_0_PROTOCOLS + value: torrent + - name: UN_SONARR_0_TIMEOUT + value: 10s + - name: UN_SONARR_0_DELETE_ORIG + value: "false" + - name: UN_SONARR_0_DELETE_DELAY + value: 5m + - name: UN_RADARR_0_URL + value: http://radarr:7878 + - name: UN_RADARR_0_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.secretName }} + key: RADARR_API_KEY + - name: UN_RADARR_0_PATHS_0 + value: /data/radarr-library + - name: UN_RADARR_0_PROTOCOLS + value: torrent + - name: UN_RADARR_0_TIMEOUT + value: 10s + - name: UN_RADARR_0_DELETE_ORIG + value: "false" + - name: UN_RADARR_0_DELETE_DELAY + value: 5m + volumeMounts: + - name: data + mountPath: /data + - name: logs + mountPath: /logs + volumes: + - name: data + hostPath: + path: {{ .Values.dogstore }}/sonarr/data + type: DirectoryOrCreate + - name: logs + hostPath: + path: {{ .Values.dogstore }}/logs/unpackerr + type: DirectoryOrCreate diff --git a/charts/media/values.yaml b/charts/media/values.yaml new file mode 100644 index 0000000..934b2f1 --- /dev/null +++ b/charts/media/values.yaml @@ -0,0 +1,41 @@ +domain: ratboo.me +certResolver: myresolver +tz: America/Los_Angeles +puid: "1000" +pgid: "1000" + +dogstore: /dogstore + +secretName: media-secrets + +storageClass: longhorn +configStorageSize: 2Gi + +plex: + image: plexinc/pms-docker:latest + advertiseIp: "https://plex.ratboo.me:443" + configSize: 20Gi + +sonarr: + image: ghcr.io/hotio/sonarr:latest + configSize: 2Gi + +radarr: + image: ghcr.io/hotio/radarr:latest + configSize: 2Gi + +bazarr: + image: lscr.io/linuxserver/bazarr:latest + configSize: 1Gi + +prowlarr: + image: ghcr.io/hotio/prowlarr:latest + configSize: 1Gi + +qbittorrent: + image: ghcr.io/hotio/qbittorrent:latest + configSize: 1Gi + webuiPort: 9191 + +unpackerr: + image: golift/unpackerr diff --git a/charts/metallb/Chart.lock b/charts/metallb/Chart.lock new file mode 100644 index 0000000..8f7965d --- /dev/null +++ b/charts/metallb/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: metallb + repository: https://metallb.github.io/metallb + version: 0.15.3 +digest: sha256:e449dd234fcbd51b21a6e63f8725dc609ab762d41db733ea7302ec44a1fb52f0 +generated: "2026-04-19T18:30:19.754244737-04:00" diff --git a/charts/metallb/Chart.yaml b/charts/metallb/Chart.yaml new file mode 100644 index 0000000..d4419e2 --- /dev/null +++ b/charts/metallb/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: metallb +description: MetalLB load balancer for internal services +version: 0.1.0 +type: application +dependencies: + - name: metallb + version: "0.15.3" + repository: "https://metallb.github.io/metallb" diff --git a/charts/metallb/charts/metallb-0.15.3.tgz b/charts/metallb/charts/metallb-0.15.3.tgz new file mode 100644 index 0000000..6bf23a7 Binary files /dev/null and b/charts/metallb/charts/metallb-0.15.3.tgz differ diff --git a/charts/metallb/templates/pool.yaml b/charts/metallb/templates/pool.yaml new file mode 100644 index 0000000..8f242ec --- /dev/null +++ b/charts/metallb/templates/pool.yaml @@ -0,0 +1,19 @@ +{{- if .Values.pool.enabled }} +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: {{ .Values.pool.name }} +spec: + addresses: + {{- range .Values.pool.addresses }} + - {{ . }} + {{- end }} +--- +apiVersion: metallb.io/v1beta1 +kind: L2Advertisement +metadata: + name: {{ .Values.pool.name }} +spec: + ipAddressPools: + - {{ .Values.pool.name }} +{{- end }} diff --git a/charts/metallb/values.yaml b/charts/metallb/values.yaml new file mode 100644 index 0000000..4f21204 --- /dev/null +++ b/charts/metallb/values.yaml @@ -0,0 +1,13 @@ +metallb: + controller: + nodeSelector: + node-role.kubernetes.io/control-plane: "true" + tolerations: + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + +pool: + enabled: false + name: internal + addresses: + - "10.0.1.250-10.0.1.250" diff --git a/charts/paperless/Chart.yaml b/charts/paperless/Chart.yaml new file mode 100644 index 0000000..ca0f031 --- /dev/null +++ b/charts/paperless/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: paperless +description: Paperless-ngx with Redis broker and PostgreSQL database +version: 0.1.0 +type: application diff --git a/charts/paperless/templates/postgres.yaml b/charts/paperless/templates/postgres.yaml new file mode 100644 index 0000000..91349fd --- /dev/null +++ b/charts/paperless/templates/postgres.yaml @@ -0,0 +1,64 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: paperless-postgres-data + labels: + app: paperless-postgres +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.postgres.storageSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: paperless-postgres + labels: + app: paperless-postgres +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: paperless-postgres + template: + metadata: + labels: + app: paperless-postgres + spec: + containers: + - name: postgres + image: {{ .Values.postgres.image }} + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: {{ .Values.postgres.database }} + - name: POSTGRES_USER + value: {{ .Values.postgres.user }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.secretName }} + key: POSTGRES_PASSWORD + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + persistentVolumeClaim: + claimName: paperless-postgres-data +--- +apiVersion: v1 +kind: Service +metadata: + name: paperless-postgres +spec: + selector: + app: paperless-postgres + ports: + - port: 5432 + targetPort: 5432 diff --git a/charts/paperless/templates/redis.yaml b/charts/paperless/templates/redis.yaml new file mode 100644 index 0000000..d6cb4ea --- /dev/null +++ b/charts/paperless/templates/redis.yaml @@ -0,0 +1,54 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: paperless-redis-data + labels: + app: paperless-redis +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.redis.storageSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: paperless-redis + labels: + app: paperless-redis +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: paperless-redis + template: + metadata: + labels: + app: paperless-redis + spec: + containers: + - name: redis + image: {{ .Values.redis.image }} + ports: + - containerPort: 6379 + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + persistentVolumeClaim: + claimName: paperless-redis-data +--- +apiVersion: v1 +kind: Service +metadata: + name: paperless-redis +spec: + selector: + app: paperless-redis + ports: + - port: 6379 + targetPort: 6379 diff --git a/charts/paperless/templates/webserver.yaml b/charts/paperless/templates/webserver.yaml new file mode 100644 index 0000000..aa6a5fb --- /dev/null +++ b/charts/paperless/templates/webserver.yaml @@ -0,0 +1,113 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: paperless-webserver + labels: + app: paperless-webserver +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: paperless-webserver + template: + metadata: + labels: + app: paperless-webserver + spec: + securityContext: + runAsUser: {{ int .Values.puid }} + runAsGroup: {{ int .Values.pgid }} + containers: + - name: paperless + image: {{ .Values.webserver.image }} + ports: + - containerPort: 8000 + env: + - name: PAPERLESS_REDIS + value: redis://paperless-redis:6379 + - name: PAPERLESS_DBHOST + value: paperless-postgres + - name: PAPERLESS_DBPASS + valueFrom: + secretKeyRef: + name: {{ .Values.secretName }} + key: PAPERLESS_DB_PASS + - name: PAPERLESS_CSRF_TRUSTED_ORIGINS + value: {{ .Values.webserver.csrfTrustedOrigins | quote }} + - name: USERMAP_UID + value: {{ .Values.puid | quote }} + - name: USERMAP_GID + value: {{ .Values.pgid | quote }} + livenessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 3 + volumeMounts: + - name: data + mountPath: /usr/src/paperless/data + - name: media + mountPath: /usr/src/paperless/media + - name: export + mountPath: /usr/src/paperless/export + - name: consume + mountPath: /usr/src/paperless/consume + volumes: + - name: data + hostPath: + path: {{ .Values.dogstore }}/paperless/data + type: DirectoryOrCreate + - name: media + hostPath: + path: {{ .Values.dogstore }}/paperless/media + type: DirectoryOrCreate + - name: export + hostPath: + path: {{ .Values.dogstore }}/paperless/export + type: DirectoryOrCreate + - name: consume + hostPath: + path: {{ .Values.dogstore }}/paperless/consume + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: paperless-webserver +spec: + selector: + app: paperless-webserver + ports: + - port: 8000 + targetPort: 8000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: paperless + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }} +spec: + rules: + - host: paperless.{{ .Values.domain }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: paperless-webserver + port: + number: 8000 diff --git a/charts/paperless/values.yaml b/charts/paperless/values.yaml new file mode 100644 index 0000000..8068a9b --- /dev/null +++ b/charts/paperless/values.yaml @@ -0,0 +1,24 @@ +domain: ratboo.me +certResolver: myresolver +tz: America/Los_Angeles +puid: "1000" +pgid: "1000" + +dogstore: /dogstore + +secretName: paperless-secrets +storageClass: longhorn + +webserver: + image: ghcr.io/paperless-ngx/paperless-ngx:latest + csrfTrustedOrigins: "https://paperless.ratboo.me" + +redis: + image: docker.io/library/redis:7 + storageSize: 1Gi + +postgres: + image: docker.io/library/postgres:15 + storageSize: 5Gi + database: paperless + user: paperless diff --git a/charts/traefik-config/Chart.yaml b/charts/traefik-config/Chart.yaml new file mode 100644 index 0000000..ccb7ae5 --- /dev/null +++ b/charts/traefik-config/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: traefik-config +description: Traefik ingress configuration for k3s (HelmChartConfig overlay + Cloudflare DNS challenge) +version: 0.1.0 +type: application diff --git a/charts/traefik-config/templates/helmchartconfig.yaml b/charts/traefik-config/templates/helmchartconfig.yaml new file mode 100644 index 0000000..95a0653 --- /dev/null +++ b/charts/traefik-config/templates/helmchartconfig.yaml @@ -0,0 +1,41 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChartConfig +metadata: + name: traefik + namespace: kube-system +spec: + valuesContent: |- + service: + spec: + loadBalancerClass: io.k3s.klipper + logs: + general: + level: WARN + ports: + web: + redirections: + entryPoint: + to: websecure + scheme: https + permanent: true + websecure: + tls: + certResolver: {{ .Values.certResolver }} + certResolvers: + {{ .Values.certResolver }}: + acme: + email: {{ .Values.acmeEmail }} + storage: {{ .Values.letsencrypt.storagePath }} + caServer: {{ .Values.acmeServer }} + dnsChallenge: + provider: cloudflare + env: + - name: CF_DNS_API_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.cloudflare.secretName }} + key: {{ .Values.cloudflare.secretKey }} + persistence: + enabled: true + storageClass: longhorn + size: 128Mi diff --git a/charts/traefik-config/templates/middleware-redirect.yaml b/charts/traefik-config/templates/middleware-redirect.yaml new file mode 100644 index 0000000..8c482a6 --- /dev/null +++ b/charts/traefik-config/templates/middleware-redirect.yaml @@ -0,0 +1,9 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https + namespace: kube-system +spec: + redirectScheme: + scheme: https + permanent: true diff --git a/charts/traefik-config/values.yaml b/charts/traefik-config/values.yaml new file mode 100644 index 0000000..fc650ed --- /dev/null +++ b/charts/traefik-config/values.yaml @@ -0,0 +1,12 @@ +domain: ratboo.me +acmeEmail: alvin7@gmail.com + +certResolver: myresolver +acmeServer: https://acme-v02.api.letsencrypt.org/directory + +cloudflare: + secretName: cloudflare-api-token + secretKey: CF_DNS_API_TOKEN + +letsencrypt: + storagePath: /data/acme.json diff --git a/charts/traefik-internal/Chart.yaml b/charts/traefik-internal/Chart.yaml new file mode 100644 index 0000000..5dd6c7f --- /dev/null +++ b/charts/traefik-internal/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: traefik-internal +description: Internal Traefik instance for LAN-only services +version: 0.1.0 +type: application diff --git a/charts/traefik-internal/templates/traefik-internal.yaml b/charts/traefik-internal/templates/traefik-internal.yaml new file mode 100644 index 0000000..922ec4c --- /dev/null +++ b/charts/traefik-internal/templates/traefik-internal.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: traefik-internal +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: traefik-internal +rules: + - apiGroups: [""] + resources: [services, endpoints, secrets, nodes] + verbs: [get, list, watch] + - apiGroups: [discovery.k8s.io] + resources: [endpointslices] + verbs: [get, list, watch] + - apiGroups: [traefik.io] + resources: [ingressroutes, ingressroutetcps, ingressrouteudps, middlewares, middlewaretcps, tlsoptions, tlsstores, traefikservices, serverstransports, serverstransporttcps] + verbs: [get, list, watch] + - apiGroups: [traefik.io] + resources: [ingressroutes/status, ingressroutetcps/status, ingressrouteudps/status] + verbs: [update] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: traefik-internal +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: traefik-internal +subjects: + - kind: ServiceAccount + name: traefik-internal + namespace: {{ .Release.Namespace }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: traefik-internal + labels: + app: traefik-internal +spec: + replicas: 1 + selector: + matchLabels: + app: traefik-internal + template: + metadata: + labels: + app: traefik-internal + spec: + serviceAccountName: traefik-internal + nodeSelector: + node-role.kubernetes.io/control-plane: "true" + tolerations: + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + containers: + - name: traefik + image: {{ .Values.image }} + args: + - --entrypoints.web.address=:{{ .Values.port }} + - --providers.kubernetescrd + - --providers.kubernetescrd.ingressClass={{ .Values.ingressClass }} + - --api.insecure=true + - --log.level=WARN + ports: + - name: web + containerPort: {{ .Values.port }} + - name: dashboard + containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: traefik-internal + annotations: + metallb.io/address-pool: internal +spec: + type: LoadBalancer + loadBalancerIP: {{ .Values.loadBalancerIP }} + selector: + app: traefik-internal + ports: + - name: web + port: {{ .Values.port }} + targetPort: {{ .Values.port }} + - name: dashboard + port: 9095 + targetPort: 8080 diff --git a/charts/traefik-internal/values.yaml b/charts/traefik-internal/values.yaml new file mode 100644 index 0000000..6dca98d --- /dev/null +++ b/charts/traefik-internal/values.yaml @@ -0,0 +1,4 @@ +image: traefik:v3.3 +port: 8082 +ingressClass: traefik-internal +loadBalancerIP: "10.0.1.250" diff --git a/charts/utils/Chart.yaml b/charts/utils/Chart.yaml new file mode 100644 index 0000000..461ece3 --- /dev/null +++ b/charts/utils/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: utils +description: Utility services -- Zerobyte backup and Seerr media requests +version: 0.1.0 +type: application diff --git a/charts/utils/templates/seerr.yaml b/charts/utils/templates/seerr.yaml new file mode 100644 index 0000000..1ca444b --- /dev/null +++ b/charts/utils/templates/seerr.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: seerr-config + labels: + app: seerr +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.seerr.storageSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: seerr + labels: + app: seerr +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: seerr + template: + metadata: + labels: + app: seerr + spec: + containers: + - name: seerr + image: {{ .Values.seerr.image }} + ports: + - containerPort: 5055 + env: + - name: TZ + value: {{ .Values.tz | quote }} + readinessProbe: + httpGet: + path: /api/v1/settings/public + port: 5055 + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 3 + livenessProbe: + httpGet: + path: /api/v1/settings/public + port: 5055 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 3 + volumeMounts: + - name: config + mountPath: /app/config + volumes: + - name: config + persistentVolumeClaim: + claimName: seerr-config +--- +apiVersion: v1 +kind: Service +metadata: + name: seerr +spec: + selector: + app: seerr + ports: + - port: 5055 + targetPort: 5055 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: seerr + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls.certresolver: {{ .Values.certResolver }} +spec: + rules: + - host: watch.{{ .Values.domain }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: seerr + port: + number: 5055 diff --git a/charts/utils/templates/zerobyte.yaml b/charts/utils/templates/zerobyte.yaml new file mode 100644 index 0000000..7d55e8a --- /dev/null +++ b/charts/utils/templates/zerobyte.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: zerobyte-data + labels: + app: zerobyte +spec: + accessModes: [ReadWriteOnce] + storageClassName: {{ .Values.storageClass }} + resources: + requests: + storage: {{ .Values.zerobyte.storageSize }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zerobyte + labels: + app: zerobyte +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: zerobyte + template: + metadata: + labels: + app: zerobyte + spec: + containers: + - name: zerobyte + image: {{ .Values.zerobyte.image }} + ports: + - containerPort: 4096 + env: + - name: TZ + value: {{ .Values.tz | quote }} + - name: BASE_URL + value: http://{{ .Values.hostIp }}:4096 + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.zerobyte.secretName }} + key: APP_SECRET + volumeMounts: + - name: data + mountPath: /var/lib/zerobyte + - name: localtime + mountPath: /etc/localtime + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: zerobyte-data + - name: localtime + hostPath: + path: /etc/localtime + type: File +--- +apiVersion: v1 +kind: Service +metadata: + name: zerobyte +spec: + selector: + app: zerobyte + ports: + - port: 4096 + targetPort: 4096 diff --git a/charts/utils/values.yaml b/charts/utils/values.yaml new file mode 100644 index 0000000..2005d9c --- /dev/null +++ b/charts/utils/values.yaml @@ -0,0 +1,14 @@ +domain: ratboo.me +certResolver: myresolver +tz: America/Los_Angeles +hostIp: "10.0.1.2" +storageClass: longhorn + +zerobyte: + image: ghcr.io/nicotsx/zerobyte:v0.33 + storageSize: 1Gi + secretName: zerobyte-secrets + +seerr: + image: ghcr.io/seerr-team/seerr:latest + storageSize: 1Gi diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..23e44d6 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ─── Configuration ─────────────────────────────────────────────────────────── +K3S_VERSION="v1.31.4+k3s1" +LONGHORN_VERSION="1.7.2" +LONGHORN_REPO="https://charts.longhorn.io" + +# ─── Helper ────────────────────────────────────────────────────────────────── +info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } +error() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2; exit 1; } + +usage() { + cat < + +Commands: + server Install k3s server (manager node) + agent Install k3s agent (worker node) + longhorn Install Longhorn via Helm + sops-keygen Generate an age keypair for SOPS + apply-secrets Decrypt and apply SOPS secrets to the cluster + deploy Helm-install all charts + all Run: server → longhorn → sops-keygen → apply-secrets → deploy +EOF +} + +# ─── k3s server ────────────────────────────────────────────────────────────── +cmd_server() { + info "Installing k3s server ${K3S_VERSION} …" + curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="${K3S_VERSION}" sh -s - server \ + --write-kubeconfig-mode 644 + + info "Waiting for node to be Ready …" + until kubectl get node "$(hostname)" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q True; do + sleep 2 + done + info "Server node is Ready." + + local token + token=$(cat /var/lib/rancher/k3s/server/node-token) + local ip + ip=$(hostname -I | awk '{print $1}') + info "Worker join command:" + echo " curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=\"${K3S_VERSION}\" K3S_URL=\"https://${ip}:6443\" K3S_TOKEN=\"${token}\" sh -" +} + +# ─── k3s agent ─────────────────────────────────────────────────────────────── +cmd_agent() { + if [[ -z "${K3S_URL:-}" || -z "${K3S_TOKEN:-}" ]]; then + error "Set K3S_URL and K3S_TOKEN environment variables first.\n K3S_URL=https://:6443 K3S_TOKEN= $0 agent" + fi + info "Installing k3s agent ${K3S_VERSION} …" + curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="${K3S_VERSION}" K3S_URL="${K3S_URL}" K3S_TOKEN="${K3S_TOKEN}" sh - + info "Agent installed. It will appear as a node shortly." +} + +# ─── Longhorn ──────────────────────────────────────────────────────────────── +cmd_longhorn() { + info "Installing Longhorn ${LONGHORN_VERSION} via Helm …" + helm repo add longhorn "${LONGHORN_REPO}" 2>/dev/null || true + helm repo update longhorn + kubectl create namespace longhorn-system 2>/dev/null || true + helm upgrade --install longhorn longhorn/longhorn \ + --namespace longhorn-system \ + --version "${LONGHORN_VERSION}" \ + --set defaultSettings.defaultReplicaCount=2 \ + --wait + info "Longhorn installed." +} + +# ─── SOPS / age ───────────────────────────────────────────────────────────── +cmd_sops_keygen() { + local keydir="/etc/sops/age" + local keyfile="${keydir}/keys.txt" + if [[ -f "${keyfile}" ]]; then + warn "Age key already exists at ${keyfile} — skipping generation." + else + info "Generating age keypair …" + sudo mkdir -p "${keydir}" + age-keygen -o "${keyfile}" 2>&1 + sudo chmod 600 "${keyfile}" + info "Key written to ${keyfile}" + fi + info "Public key (put this in .sops.yaml):" + grep 'public key' "${keyfile}" | awk '{print $NF}' +} + +cmd_apply_secrets() { + local secrets_dir + secrets_dir="$(cd "$(dirname "$0")/.." && pwd)/secrets" + info "Decrypting and applying secrets …" + local decrypted + decrypted="$(sops -d "${secrets_dir}/secrets.enc.yaml")" + + local ns + for ns in $(printf '%s\n' "${decrypted}" | grep '^\s*namespace:' | awk '{print $2}' | sort -u); do + kubectl create namespace "${ns}" 2>/dev/null || true + done + + printf '%s\n' "${decrypted}" | kubectl apply -f - + info "Secrets applied." +} + +# ─── Deploy all charts ────────────────────────────────────────────────────── +cmd_deploy() { + local charts_dir + charts_dir="$(cd "$(dirname "$0")/.." && pwd)/charts" + + local -a chart_order=(traefik-config media paperless mealie dashboards utils) + local -A chart_ns=( + [traefik-config]=kube-system + [media]=media + [paperless]=paperless + [mealie]=apps + [dashboards]=apps + [utils]=apps + ) + + for chart in "${chart_order[@]}"; do + local ns="${chart_ns[$chart]}" + info "Deploying ${chart} → namespace ${ns} …" + kubectl create namespace "${ns}" 2>/dev/null || true + helm upgrade --install "${chart}" "${charts_dir}/${chart}" \ + --namespace "${ns}" \ + --wait --timeout 5m + done + info "All charts deployed." +} + +# ─── Main ──────────────────────────────────────────────────────────────────── +[[ $# -lt 1 ]] && { usage; exit 1; } + +case "$1" in + server) cmd_server ;; + agent) cmd_agent ;; + longhorn) cmd_longhorn ;; + sops-keygen) cmd_sops_keygen ;; + apply-secrets) cmd_apply_secrets ;; + deploy) cmd_deploy ;; + all) cmd_server; cmd_longhorn; cmd_sops_keygen; cmd_apply_secrets; cmd_deploy ;; + *) usage; exit 1 ;; +esac diff --git a/secrets/secrets.enc.yaml b/secrets/secrets.enc.yaml new file mode 100644 index 0000000..6847bef --- /dev/null +++ b/secrets/secrets.enc.yaml @@ -0,0 +1,158 @@ +#ENC[AES256_GCM,data:1wyYmxw5bF4pkfLGKuBx7V0KqV9MOj+WEcNkTLX2o8cjN7k/NJg2YhPkDKbxQhalm5RccC3vCCgmaeJzjExt,iv:v/VlwHMwJSHfevf2sN8Sq9dMUdah3cNWitmWR6OUJm8=,tag:Q/M3rkS+NLlHCT0Mnu1TLA==,type:comment] +#ENC[AES256_GCM,data:ZL4tevfMjoN4RMiNz1zQHAiZ3cpcfPKFwirJWPwf7qWf7fd8jWGr1Fr/KuFiIzSkny3n9nC3Vwg=,iv:YKPVLd4zxA7wMFx0jd917W4pHSRtcXP93rcBz/0jyso=,tag:G3Hi0oXtz+/9tE4zRYPUMA==,type:comment] +#ENC[AES256_GCM,data:IQpAXI8AERzfGJSWcnGnBKustu2wkhf7CtehJLk2qhOj9R5X8QoIb7AEfhy0ONi8EpZSfKIQ5AbYom+ZjB9vUiDKeCmtae1RpQVXHVu7jmh9OI212+8VkUyi3PDMN3wH7NIQ1zX8h5PxYFUuKvWgx4hHCYESwgfkW49fSTfwxeeWjSbyNOM=,iv:69fsTOQ5LJ3s4KRwaOb676dAOaujpWx3Ah+q8mlzzf0=,tag:b/nxflYxTCaiInYsb7Szpw==,type:comment] +#ENC[AES256_GCM,data:Rd8TnilJ/5Ak7egTQsvVeXgCx7Gx3EmYMptVWqhtb6hVJ5I0uHNdMlBe1YGCPfic8F0fYfzBlgJaNz1ycfooQfF/lYfBxGx9EARSGznEsg==,iv:0SRR76k5qnbu4j+dMuUf2kLMeZAkerBmej0gR8/Wpfg=,tag:HMaPbx1ExnzRoiq6DlxYIw==,type:comment] +# +#ENC[AES256_GCM,data:y3lu8sygTZJ+TSJOyrFXShJGBu5gHYLT4A==,iv:xmugUAyc116U4nESzKAYcvz76BBGwHLK7HE61gnS2gE=,tag:1LTSMRi+Aat4hk2N7c+2gw==,type:comment] +#ENC[AES256_GCM,data:/ny7h4o0XSF1kROKuEUD9xrZJYOJ7mTZGIgTtIWHdclmdd9vYTLNl2+Hb9kHzCCWUgPXRg6pOww=,iv:47xT9Fct8sJrfIf8qRGD1xUTEkgdGf+KC9hWQ3XJG44=,tag:yix6VVO+g10cLbUwLWdTeA==,type:comment] +#ENC[AES256_GCM,data:du0a4InEiMTw4mg5EqVPpANM3j0FNpDSle2na/7Q0vM1Z4SE52Jvvz3ZIV99b1pY30QlFdnTGO/4grVJd7xiUYC5wCUNY/uGNYGX2HzOWZjByEb5p9VCOPS2Czl/pPGM9+9d08mn2u1Ww4Wt6a/hrFOUWQddjTNz7HwG66xT/YVTnCN++mTnkCbgQKIjV1YhpSQKWA5jNfgSqSVKwbEA60JvESavT51HSZnrAGKWIepaJc7S14jTp+tUdqA=,iv:SZKsksvhtXCcKByjV85stfmt/DGHSsSh+PTGuCfe874=,tag:IQWtYVrLFx8EfPtKb1tK9g==,type:comment] +apiVersion: ENC[AES256_GCM,data:3lY=,iv:MBpXRZ3rElkVBxAlvxmzORMCq0G87jB3Ik11tWMfz64=,tag:oPPwCxOiK3ePCWRhtHmN2w==,type:str] +kind: ENC[AES256_GCM,data:8+Toxlr5,iv:DTaS/GEKGLtZsHQBYTGaQZYSp9mr/A5Vbuqi7uq46rU=,tag:1YvMaF1/eLNkZv0F3Du0zA==,type:str] +metadata: + name: ENC[AES256_GCM,data:RgUAwk6jY7DHv+CbTtyXx5nzEQQ=,iv:FeAx14K1od0ehMnbcpnD4FW1bpEF/3M+dq8NGoJwTJA=,tag:SziXyyeAgShOgsandPQD7A==,type:str] + namespace: ENC[AES256_GCM,data:IgxkBPTfQ8rEWzA=,iv:56aP5rBeH9aupBi338/9r4fXZ1ova8ubO7N8elr2/qM=,tag:vfFTXJrQ2J5vln/rBGl/og==,type:str] +type: ENC[AES256_GCM,data:Efo/5l3N,iv:moQ775VrJkfQFO3YPM3ZT77i0IzcWy/G7b5E4/XOyJY=,tag:zfEMZgXgO+qMQIvPrLeMsA==,type:str] +stringData: + CF_DNS_API_TOKEN: ENC[AES256_GCM,data:74wQ12RQ9M7BckVfPjbRUnEbeaFB0aOPIhZHrFo=,iv:SIS0dJBpXkcJLjqEGnM1mtD1CB76MN2rZObyG8bCVe4=,tag:Rac0lPa5J2JSkdKcVc1PtQ==,type:str] +sops: + age: + - recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS + Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND + Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz + eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5 + oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-19T21:05:51Z" + mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2 +--- +#ENC[AES256_GCM,data:ibx/1sx8LotfzeKT/rRsedIOoYD+F6aXbrA3QEqjfSrFAXWchGiLX9ddAY0q0DOGi7tFcty1rwWUj1Iiy+vd88zZDRhLSVgs0hIdqqPaHVubOWA7gAmxvZdb1faLU4cOAe7gL9bUdaBlGzQKat6miytnWBXL8qidoFl/Ps1wNNDXRBpe+TsQfVAp5T5jirq35s7GTeXUbQO3bNkKx/ZNHmV7ASctKeRv5TLuAJIwyU6UIGSSnx6Sdr43mZlm6iJjdoT6jQxeH7yB1Gut7g==,iv:wAtBwQNTI56JLHISsA1W00M3X+v6R6vEkNK1SVULsTg=,tag:Jk/4OdnXyV6/5VwFcXpX/A==,type:comment] +apiVersion: ENC[AES256_GCM,data:jNM=,iv:dt+kXGEjK6yS09KRE5I3uAXNLGN05RbO0GyjNTHYHtk=,tag:7z93Yh4m2SpZLHdxVIr1hw==,type:str] +kind: ENC[AES256_GCM,data:ORSVG3Tn,iv:gqhsNppUalHTDcKa7q/P3TR5t4VZC1gLc2MgY8V6xEw=,tag:vr764iOVWPJujZd4rxRYKA==,type:str] +metadata: + name: ENC[AES256_GCM,data:1ngffZeDVnrLbux2pg==,iv:OXYS/A5PBvmIVw5qDu+Um7J/JhM8shVfP3KDIXNBCU0=,tag:wfmayiCBy22O+WIPBC3PPQ==,type:str] + namespace: ENC[AES256_GCM,data:xCM+W3I=,iv:0KLfoYx8Q+NVsm84KZX3tClbWdwuTpI6/pD7HQMK3Fk=,tag:hSFwqHfnHWOPvesUhWyo+Q==,type:str] +type: ENC[AES256_GCM,data:K3TlGbY8,iv:ZKm+PyQ4IAP0K1ymcMvWPOEUgUGzH93UkmH+rheJk7A=,tag:8tMEVI895tOOt8/tjiKRuw==,type:str] +stringData: + PLEX_CLAIM: ENC[AES256_GCM,data:A+dwGU//qfy6sBoww5c36xs2WcwdbQY=,iv:S6wOadxaC4ITyZN/7u5Lcu8AeBFdQLRqRlCHH3oSj28=,tag:hlNKdZGzURuFgcSrUrZyJA==,type:str] + SONARR_API_KEY: ENC[AES256_GCM,data:VvU91ZNxv6tauBEyK9j8THxT8zw0mTLEmTTSRgYDEek=,iv:DZr5JL9T4f7XwQ03jkwVKfWA/xyAZlo4f/BKAzIIsQo=,tag:mCh8ExJvuoltcMlzAk6jpw==,type:str] + RADARR_API_KEY: ENC[AES256_GCM,data:UJr87uk6V8sYT3kIp8dzevE8xA55QN4Aop6nP3uMBbc=,iv:Z0NicMtVOBxEwXtcBjqfMZKuTrPjqi3H1vIYu6llHHo=,tag:Xz88jgAwwK/ekh+8udlCjQ==,type:str] +sops: + age: + - recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS + Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND + Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz + eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5 + oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-19T21:05:51Z" + mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2 +--- +#ENC[AES256_GCM,data:rmGnwFT8rj+qcuyiuf4VdY5KJP5m4bMt9a1znHpQ+uYyrfzNoAH0eqn6yHMB3xAwFJtFOkzEdd0E1PaL5C30UbqtpwDNyzD0QYipIwDkQBGwRnkXXI8xw1ftE1zHcFWnPmnSH70hCJa+/Xu425pIyJeVlJhUqoOGkG5/gV0DQoVMnJXFXnh5a64Ggv7UzB92o9/iyUFMgi/7j4ePNQCc7Um1VePXc5mkRl8cQBbvwAgyMZo+IxDJ8hpS2HgbRGey5/RAxaDvRbwkyRU=,iv:Cl77wg8+J3D6QrIgbjRS/lzbJLf2qGkK/AtnIOoKG+E=,tag:m4TfMvHm5W612rrJJ+ytkA==,type:comment] +apiVersion: ENC[AES256_GCM,data:fyA=,iv:lkH+XfaHDNUNE3+oUW5lkA3ev0AEQZGe2y/J5H5G4AI=,tag:QCGr4Ia7Ea+CRWWoWSFaug==,type:str] +kind: ENC[AES256_GCM,data:GS9uiEv6,iv:9VAdvjoF7thUVtJRpyaDnBOVlZ9so1p5f4iaw1WxJ0w=,tag:gDCWZfn4zCdyRdMup9Vs3Q==,type:str] +metadata: + name: ENC[AES256_GCM,data:XLV37KdISRoLUp4yo44=,iv:Hb2AQVYhhu5erfg+41edOfB4cT6O6e2k1ytEixVaZDk=,tag:2qP4C7sxSdkdnsTVRhsXcg==,type:str] + namespace: ENC[AES256_GCM,data:S/sYVQ==,iv:SuUw6GPbT7YF66+O2w8al89NcJBr3oi8C12nZIbNWWc=,tag:NBVJrXnAhVJqx325MQnOTQ==,type:str] +type: ENC[AES256_GCM,data:suP66pVq,iv:RrLuX+MOixEaR3iw4EMBCqkT03TW3xvmzdXsTf8kl2c=,tag:Id8xGXmFcmN+gYyZsohp3g==,type:str] +stringData: + OPENAI_API_KEY: ENC[AES256_GCM,data:1QtOURL+C0DcB4AQhvmQP2PqeNQm2VQWTr/alY4FQzbhCnpxSFLS,iv:hy+a7cAcUNYr1XkKK1JoG8imjh5kT7B2tr5FWfHgAds=,tag:72zQFszAQpPEzZ0v24V5yg==,type:str] +sops: + age: + - recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS + Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND + Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz + eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5 + oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-19T21:05:51Z" + mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2 +--- +#ENC[AES256_GCM,data:3I2e5Uv2dvWtDWXnVh0AULaS9z2Pcd/iUHjvycTsfxhDpzEJfc0a0dISctuLpELEyBv1PtYemFZ/XeTqGJswTF4vvzM7UL6scPezIm674aOtjVqPj7/C3nQgfnuY1qgVY7duawWZeaozf8hBKef1JJ1qR1T3Mati3YBIaw3fp1qjYyKo47/F19UT/qtL6mlu8CE3/zIVbzZGsKwfqhYf6oLg2I7aNlvJ0/yWz8RSjwmtWj8WzUD1wZh+DnFRiTlbE+kLdgw=,iv:i+4OhA58TOv1pzPInqu9qi9zunYzpNbvHrELXVsXis8=,tag:XJyBP8nci2u5X7THGox3CQ==,type:comment] +apiVersion: ENC[AES256_GCM,data:RdI=,iv:YlI7VI5Tk6f99ZhiJEI/LaGgmYejt6/8k0wo+n8G19E=,tag:WY473Ft6vXo0NkpGlnGq/w==,type:str] +kind: ENC[AES256_GCM,data:5fh5Zy1D,iv:1qApXn+j2LFNs1fzrH8j6M84espcQz4cHwquEmsHDSU=,tag:MnWyue/omxF6Emrpsz9eEw==,type:str] +metadata: + name: ENC[AES256_GCM,data:ZYLasrZQ1Bu0jcLbRQjmjCo=,iv:O0uBw2j+X0CPaUkYQvnTVRgl9nNZBDF058/hZB/WFwY=,tag:xuwCgyi5VT9enQ3HlYzW1g==,type:str] + namespace: ENC[AES256_GCM,data:Xr9+LDzWK7z+,iv:6iXdpkcUho23jU2BZLHKkxONTtQIYkZem9NVlyB/Ltk=,tag:3wV3EMtmqXr0yTUcfbqxCA==,type:str] +type: ENC[AES256_GCM,data:+5bc8NF8,iv:JpVUvDJZdk++m1K5pyuPTF1p17X62Mlwv6GKK9Hqoz8=,tag:Kdihg3qXXHD0ieypkcvWSg==,type:str] +stringData: + PAPERLESS_DB_PASS: ENC[AES256_GCM,data:5/oF170q34nO,iv:y09u1KOZaIoUNtDog0sEbkj5gKD8C3JQyvNDSy0ElkI=,tag:WvIMb0Z0l7XXC6wB59ZTtg==,type:str] +sops: + age: + - recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS + Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND + Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz + eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5 + oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-19T21:05:51Z" + mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2 +--- +#ENC[AES256_GCM,data:txDOIoubztyTgbgUC4MMNxNIwDkjtF/m+H5YAZ6hdaHTCBjVZSVKhHjYJzLFPeHE2EL02SEgJaXSkyF0bWwWw/QfR/yCkiGPdZ0fAFDKtJJBtITkM4O5rwRCTEXsQXleWK5NM6M55s5ElWIlBE//uLt2+hPlDQv26EOwwAa9I7kW2bUPDGvYNJVc7mJSfjMQb6aOx1MzrA+PUryeBVdK0xdmBmiQaZyGC0MexEjBwAycVnCM,iv:u4rMeGWy1J779Z7JNFvWTsxDREbTWAS+bXFxQhPTyGA=,tag:mVoS7lSVAHcqPcQ6iqhjvA==,type:comment] +apiVersion: ENC[AES256_GCM,data:JGU=,iv:vr1Lkm9BBG2u8Ay0PGAIMTYUUKhMHIho3mVP1lY6it0=,tag:12JH+n4nUsYqmiy1+CnwfA==,type:str] +kind: ENC[AES256_GCM,data:VLdoeXnN,iv:eH+/KpTMtpclAxFnjX5mXxkF73HUMJlskNSS/iW+g+U=,tag:NE/Xdc9qsfFmXAyZ4y+3HQ==,type:str] +metadata: + name: ENC[AES256_GCM,data:dKvmrGqwCK+KvfdBxcs=,iv:3B71qX/mVAJGMcKlxTrnfub3rp/o7PJ+mquxo+V6svY=,tag:gnvbDBvZexdbOHkQQNUInw==,type:str] + namespace: ENC[AES256_GCM,data:93lKXQ==,iv:TQq2ZC7l1uQBc0FNRg6sQRfLuLIokQHpgAzRTcFmGsE=,tag:Mv+ih+YMDfYIihWNgnztow==,type:str] +type: ENC[AES256_GCM,data:Ep25FUNF,iv:ZCzL99uhG0SYRXlu9j3GmgWtIxcfe6C9lEkP5EFr6SY=,tag:ANEtn7vXJisqoUlX7rEBAQ==,type:str] +stringData: + ADGUARD_PASSWORD: ENC[AES256_GCM,data:Keh2GHhvfSyp9Q==,iv:bJ2CdmjqMZUSVw2T1jerqT1gkiP6k+aL9VyGCVJ10wI=,tag:cfR6jRn6NyrZ3/2WM5SdKg==,type:str] +sops: + age: + - recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS + Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND + Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz + eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5 + oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-19T21:05:51Z" + mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2 +--- +#ENC[AES256_GCM,data:CXcPJbTR4tVVRGK6l0NBowzab2DHaufhxX++4dQ9/S3Q9V0Me/kA0xBoR4PlfFRVG6QgsIeEK1ysx3d5qOy6/VcS9HFibUv8lTx0X8EonLNgsc2Qc4V526eJ3tByTyqRw780YsTVZFHZ+ofJBNPAcB4Rls1G8214E/kfyWxKWdHB7Joy83SQeWgL6On2oEdeWnpyp+E7FTqk2SXR+/YLVnNbixeirjpRCWlcnPXxhoSlRzcxUmAWTPT8zWicoUo5AiSh2C/DNw==,iv:lZ1ffJ4ixyhY829s2EiJlcF18fK1RDSUZ1F1Lw+YG14=,tag:dfn5m/iYHvzO7BE+ZygMMw==,type:comment] +apiVersion: ENC[AES256_GCM,data:F6o=,iv:8McPTAtRKlG0wpF1DUXRrhkzNuoD97Vu4OFyI8Opy28=,tag:DgEjMs6yXKFEv6Uu8A8WDg==,type:str] +kind: ENC[AES256_GCM,data:eVSn4ODQ,iv:iDOb7kRnWbW1CYKILAZwbtlhbAqwi/I+YXFbHsmz2KI=,tag:dcWY/PdP2eMRv1HxmfyHoQ==,type:str] +metadata: + name: ENC[AES256_GCM,data:JffgDJUP/W+tue33DF91Ug==,iv:pXq1HZ2azFQSYP1LKYl94q8f1srLOH7GJw1cW6p+yrI=,tag:Dh6BCtR0gxqnb+c20RrKLA==,type:str] + namespace: ENC[AES256_GCM,data:O6kz8Q==,iv:ZMv7m+YLaIChgNTM4Riopt2VUNg5HwUwdLR6bRA1Nf0=,tag:undk4ODEabPJbQKoa1He7A==,type:str] +type: ENC[AES256_GCM,data:YMUJyMI2,iv:o++4jFOch8C8g5iKCzot/AcHnERRO/Yqn/uHuCAIFEI=,tag:ReJgAAajctyGo7xYr2Yc8w==,type:str] +stringData: + ZEROBYTE_APP_SECRET: ENC[AES256_GCM,data:cqkj4VGIbbV9pvkZf+GHC9WWB07I/4pyu3ML7Ld/CiTaJB04WzsTwTi2bzxrfZopJc76gvXe1d55Kkm34fQ=,iv:w7WgywKZfMLcgexyJUvhpXnIhc+Xns1RDJXhHx/vBKc=,tag:X/Xx9PSs69gGY9LIkstB5g==,type:str] +sops: + age: + - recipient: age12gv2cu66v80khwse5jgwcaukf3juvufkm2kw507gfnvecdpwt3hsjra7te + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWFcrTlYrOW1Tb0xGUmVS + Q3VPb3VPMS9hRWQ4aDQzRGtFTXp4SU90YWlrCng5NkN3TUFEUGIrWkRCK1NMeVND + Z2RwV0JKVnRTMWUvWlpDRzhBQWtsNVkKLS0tIHZ2NkZaVTJSaE1vTjVVMXhzTmYz + eGZTZ0VSUElFZVpqWlVISjNYdnA4UFUK/uOyj7CKU0XLHHdPNKByO2c56JWQfhk5 + oauimeYrkNE+06dhXgVcJiQH+HcB33tB9u3YS9LxFYs3R98zKAHG6g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-19T21:05:51Z" + mac: ENC[AES256_GCM,data:GQx4YuPIIfZxRpWtHCa8pCtidtmdYoIdMsK0dQJO42XT9KR5lbwNaP3v4GoaDoeM+L7iAn2OprpE54KkwIwRfb3NAjTeUvXO+J/Yi4ZJnLtuTOlAZrC8YvjmiZ6DaL8pvpRz7VUCfzNOoyrbjSJ2Qv/PWAUENcDEU7yOHNv7RBA=,iv:FqYd2F4vBqSCeKPsrWY/a8RePgkU2aP9cadB6nMyWaQ=,tag:FC75r3bMOQDZZwO0qJi4xg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2