From 85c916285e84d773284cc8139b63b43f184c4f98 Mon Sep 17 00:00:00 2001 From: Scooby Husky Date: Mon, 17 Aug 2026 22:31:41 -0500 Subject: [PATCH] Phase 2: deploy n8n and Nextcloud warm standbys on the VPS Same CNPG replica-cluster pattern as Authentik (continuous WAL replay from home via VPS MinIO, app at 0 replicas until manual promotion - see infrastructure/vps-standby/authentik/manifests/cnpg-cluster.yaml for the full rationale). n8n: N8N_ENCRYPTION_KEY copied byte-identical from home (kubectl, not git) - decrypts stored credentials in the replicated DB, same reasoning as Vault's unseal key / root token copies. Nextcloud: adds infrastructure/vps-standby/nextcloud/manifests/ pvc-restore-cronjob.yaml, the read side of home's existing nextcloud-pvc-sync restic backup - restores the latest snapshot from VPS MinIO into this standby's PVC daily at 04:00 (2h after home's 02:00 backup). No Redis on the VPS side (no redis-operator deployed there, out of scope for a standby that isn't serving traffic - Nextcloud degrades gracefully to DB-based locking without it). --- argocd-apps/vps-standby/n8n.yaml | 33 ++++++ argocd-apps/vps-standby/nextcloud.yaml | 33 ++++++ .../n8n/manifests/cnpg-cluster.yaml | 57 ++++++++++ infrastructure/vps-standby/n8n/values.yaml | 105 ++++++++++++++++++ .../nextcloud/manifests/cnpg-cluster.yaml | 56 ++++++++++ .../manifests/pvc-restore-cronjob.yaml | 101 +++++++++++++++++ .../vps-standby/nextcloud/values.yaml | 94 ++++++++++++++++ 7 files changed, 479 insertions(+) create mode 100644 argocd-apps/vps-standby/n8n.yaml create mode 100644 argocd-apps/vps-standby/nextcloud.yaml create mode 100644 infrastructure/vps-standby/n8n/manifests/cnpg-cluster.yaml create mode 100644 infrastructure/vps-standby/n8n/values.yaml create mode 100644 infrastructure/vps-standby/nextcloud/manifests/cnpg-cluster.yaml create mode 100644 infrastructure/vps-standby/nextcloud/manifests/pvc-restore-cronjob.yaml create mode 100644 infrastructure/vps-standby/nextcloud/values.yaml diff --git a/argocd-apps/vps-standby/n8n.yaml b/argocd-apps/vps-standby/n8n.yaml new file mode 100644 index 0000000..96ae655 --- /dev/null +++ b/argocd-apps/vps-standby/n8n.yaml @@ -0,0 +1,33 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vps-n8n + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "3" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: vps-standby + sources: + - repoURL: https://riatlas.github.io/chart__n8n + chart: n8n + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/vps-standby/n8n/values.yaml + - repoURL: https://gitlab.kube.huskypup.net/Scooby/Homelabv4.git + targetRevision: main + ref: values + - repoURL: https://gitlab.kube.huskypup.net/Scooby/Homelabv4.git + targetRevision: main + path: infrastructure/vps-standby/n8n/manifests + destination: + name: vps-standby + namespace: n8n + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/argocd-apps/vps-standby/nextcloud.yaml b/argocd-apps/vps-standby/nextcloud.yaml new file mode 100644 index 0000000..ec40fc9 --- /dev/null +++ b/argocd-apps/vps-standby/nextcloud.yaml @@ -0,0 +1,33 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vps-nextcloud + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "3" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: vps-standby + sources: + - repoURL: https://nextcloud.github.io/helm/ + chart: nextcloud + targetRevision: "*" + helm: + valueFiles: + - $values/infrastructure/vps-standby/nextcloud/values.yaml + - repoURL: https://gitlab.kube.huskypup.net/Scooby/Homelabv4.git + targetRevision: main + ref: values + - repoURL: https://gitlab.kube.huskypup.net/Scooby/Homelabv4.git + targetRevision: main + path: infrastructure/vps-standby/nextcloud/manifests + destination: + name: vps-standby + namespace: nextcloud + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/infrastructure/vps-standby/n8n/manifests/cnpg-cluster.yaml b/infrastructure/vps-standby/n8n/manifests/cnpg-cluster.yaml new file mode 100644 index 0000000..0022f95 --- /dev/null +++ b/infrastructure/vps-standby/n8n/manifests/cnpg-cluster.yaml @@ -0,0 +1,57 @@ +--- +# CNPG replica cluster continuously replaying WAL from home's pg-n8n +# (apps/n8n/manifests/cnpg-cluster.yaml) via VPS MinIO's +# cnpg-backups/pg-n8n bucket path. Same pattern as +# infrastructure/vps-standby/authentik/manifests/cnpg-cluster.yaml - see +# that file's comments for the full rationale (replica-cluster mode vs. +# custom restore scripting, read-only until manual promotion). +# +# vps-minio-secret is a plain Secret copied here manually (kubectl, not +# git), same as the authentik one: +# kubectl -n n8n create secret generic vps-minio-secret \ +# --from-literal=accesskey= \ +# --from-literal=secretkey= +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-n8n + namespace: n8n +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 1 + + resources: + requests: + memory: "256Mi" + cpu: "25m" + limits: + memory: "1Gi" + cpu: "250m" + + storage: + size: 10Gi + storageClass: local-path + + bootstrap: + recovery: + source: home-backup + + externalClusters: + - name: home-backup + barmanObjectStore: + destinationPath: s3://cnpg-backups/pg-n8n + endpointURL: http://vps-minio.minio.svc.cluster.local:9000 + s3Credentials: + accessKeyId: + name: vps-minio-secret + key: accesskey + secretAccessKey: + name: vps-minio-secret + key: secretkey + + replica: + enabled: true + source: home-backup + + monitoring: + enablePodMonitor: false diff --git a/infrastructure/vps-standby/n8n/values.yaml b/infrastructure/vps-standby/n8n/values.yaml new file mode 100644 index 0000000..ef759ee --- /dev/null +++ b/infrastructure/vps-standby/n8n/values.yaml @@ -0,0 +1,105 @@ +# n8n warm standby on the VPS - Phase 2. Same discipline as +# infrastructure/vps-standby/authentik/values.yaml: the CNPG replica +# cluster keeps the DB continuously warm in the background, but the app +# itself stays at replicaCount: 0 until a deliberate manual promotion +# (flip pg-n8n's spec.replica.enabled to false, then scale this up). +# +# N8N_ENCRYPTION_KEY must be byte-identical to home's - it decrypts +# stored credentials (API keys, OAuth tokens, etc.) that live encrypted +# in the DB being replicated. Unlike Authentik's chart-deterministic +# secret_key, n8n's isn't derived from anything reproducible - it's +# copied for real (kubectl, not git, same as Vault's unseal key / +# root token): +# kubectl -n n8n create secret generic n8n-config-secret \ +# --from-literal=encryption-key= +image: + repository: n8nio/n8n + tag: "2.0.3" + +config: + database: + type: postgresdb + postgresdb: + host: pg-n8n-rw + port: 5432 + database: n8n + user: n8n + schema: public + generic: + timezone: America/New_York + path: / + host: n8n.kube.huskypup.net + port: 5678 + protocol: https + executions: + mode: regular + saveDataOnError: all + saveDataOnSuccess: all + saveDataManualExecutions: true + pruneData: true + pruneDataMaxAge: 3760 + +secret: {} + +# Not run in normal operation - see file header. +replicaCount: 0 + +service: + type: ClusterIP + port: 80 + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + +ingress: + enabled: false + +persistence: + enabled: true + type: existing + existingClaim: n8n-main-persistence + storageClass: local-path + accessModes: + - ReadWriteOnce + size: 10Gi + +resources: + requests: + cpu: 50m + memory: 256Mi + limits: + cpu: "500m" + memory: 512Mi + +extraEnvSecrets: + DB_POSTGRESDB_PASSWORD: + name: pg-n8n-app + key: password + N8N_ENCRYPTION_KEY: + name: n8n-config-secret + key: encryption-key + +extraEnv: + WEBHOOK_URL: https://n8n.kube.huskypup.net/ + N8N_EDITOR_BASE_URL: https://n8n.kube.huskypup.net + N8N_LOG_LEVEL: error + +postgresql: + enabled: false + +redis: + enabled: false + +scaling: + enabled: false + +extraVolumes: [] +extraVolumeMounts: [] +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/infrastructure/vps-standby/nextcloud/manifests/cnpg-cluster.yaml b/infrastructure/vps-standby/nextcloud/manifests/cnpg-cluster.yaml new file mode 100644 index 0000000..d8b7f6f --- /dev/null +++ b/infrastructure/vps-standby/nextcloud/manifests/cnpg-cluster.yaml @@ -0,0 +1,56 @@ +--- +# CNPG replica cluster continuously replaying WAL from home's pg-nextcloud +# (apps/nextcloud/manifests/cnpg-cluster.yaml) via VPS MinIO's +# cnpg-backups/pg-nextcloud bucket path. Same pattern as the authentik/n8n +# VPS replica clusters - see infrastructure/vps-standby/authentik/ +# manifests/cnpg-cluster.yaml for the full rationale. +# +# vps-minio-secret is a plain Secret copied here manually (kubectl, not +# git): +# kubectl -n nextcloud create secret generic vps-minio-secret \ +# --from-literal=accesskey= \ +# --from-literal=secretkey= +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-nextcloud + namespace: nextcloud +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 1 + + resources: + requests: + memory: "256Mi" + cpu: "25m" + limits: + memory: "1Gi" + cpu: "250m" + + storage: + size: 20Gi + storageClass: local-path + + bootstrap: + recovery: + source: home-backup + + externalClusters: + - name: home-backup + barmanObjectStore: + destinationPath: s3://cnpg-backups/pg-nextcloud + endpointURL: http://vps-minio.minio.svc.cluster.local:9000 + s3Credentials: + accessKeyId: + name: vps-minio-secret + key: accesskey + secretAccessKey: + name: vps-minio-secret + key: secretkey + + replica: + enabled: true + source: home-backup + + monitoring: + enablePodMonitor: false diff --git a/infrastructure/vps-standby/nextcloud/manifests/pvc-restore-cronjob.yaml b/infrastructure/vps-standby/nextcloud/manifests/pvc-restore-cronjob.yaml new file mode 100644 index 0000000..3c5caf0 --- /dev/null +++ b/infrastructure/vps-standby/nextcloud/manifests/pvc-restore-cronjob.yaml @@ -0,0 +1,101 @@ +--- +# Restores the latest restic snapshot from home's nextcloud-pvc-sync +# CronJob (apps/nextcloud/manifests/pvc-sync-cronjob.yaml, which backs up +# to VPS MinIO's nextcloud-files/restic-repo daily at 02:00) into this +# standby's own Nextcloud PVC. Runs daily at 04:00 - enough margin after +# home's job to be sure that day's snapshot has landed. +# +# `restic restore latest --target /` restores into /data/... because +# home's backup stored an absolute /data path (`restic backup /data ...`) +# - mounting the destination PVC at /data here mirrors that exactly. +# +# nextcloud-restic-password must be the SAME password used to init the +# repo at home (it's the decryption key for the whole restic repository, +# not something that can differ per-consumer) - copied here manually +# (kubectl, not git): +# kubectl -n nextcloud create secret generic nextcloud-restic-password \ +# --from-literal=password= +# +# Nextcloud itself isn't running (replicas: 0, see values.yaml) so there's +# no live-write conflict risk overwriting the PVC on every run. +apiVersion: batch/v1 +kind: CronJob +metadata: + name: nextcloud-pvc-restore + namespace: nextcloud +spec: + schedule: "0 4 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65534 + fsGroup: 33 # matches the real nextcloud Deployment's fsGroup, same reasoning as home's pvc-sync-cronjob + seccompProfile: + type: RuntimeDefault + containers: + - name: restic-restore + image: restic/restic:0.16.4 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + command: + - /bin/sh + - -c + - | + set -eu + export RESTIC_REPOSITORY="s3:http://vps-minio.minio.svc.cluster.local:9000/nextcloud-files/restic-repo" + export RESTIC_PASSWORD_FILE=/restic-secret/password + export AWS_ACCESS_KEY_ID="${MINIO_ACCESS_KEY}" + export AWS_SECRET_ACCESS_KEY="${MINIO_SECRET_KEY}" + export RESTIC_CACHE_DIR=/tmp/restic-cache + + if ! restic snapshots >/dev/null 2>&1; then + echo "No repo/snapshots reachable yet - nothing to restore." + exit 0 + fi + + echo "==> Restoring latest snapshot into /data..." + restic restore latest --tag nextcloud --host nextcloud-k8s --target / + + echo "==> Done." + env: + - name: MINIO_ACCESS_KEY + valueFrom: + secretKeyRef: + name: vps-minio-secret + key: accesskey + - name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: vps-minio-secret + key: secretkey + volumeMounts: + - name: nextcloud-data + mountPath: /data + - name: restic-secret + mountPath: /restic-secret + readOnly: true + volumes: + - name: nextcloud-data + persistentVolumeClaim: + # Chart-generated PVC name, follows the Helm release name + # (Application metadata.name: vps-nextcloud) - same + # release-name-based naming gotcha hit with vault-0/ + # vps-vault-0 and gitea-http/vps-gitea-http. Verify against + # `kubectl -n nextcloud get pvc` after first deploy. + claimName: vps-nextcloud-nextcloud + - name: restic-secret + secret: + secretName: nextcloud-restic-password + items: + - key: password + path: password diff --git a/infrastructure/vps-standby/nextcloud/values.yaml b/infrastructure/vps-standby/nextcloud/values.yaml new file mode 100644 index 0000000..19170f0 --- /dev/null +++ b/infrastructure/vps-standby/nextcloud/values.yaml @@ -0,0 +1,94 @@ +# Nextcloud warm standby on the VPS - Phase 2. Same discipline as the +# other vps-standby apps: CNPG replica cluster (manifests/cnpg-cluster.yaml) +# keeps the DB warm, manifests/pvc-restore-cronjob.yaml keeps file content +# warm, but the app itself stays at replicaCount: 0 until a deliberate +# manual promotion. +# +# No Redis here - the home instance uses the redis-operator +# (infrastructure/vps-standby has no redis-operator deployed, out of +# scope for a standby that isn't actually serving traffic). Nextcloud +# runs fine without Redis (falls back to DB-based locking, just slower) - +# acceptable for an emergency-promotion scenario; add a real Redis at +# promotion time if desired. +replicaCount: 0 + +nextcloud: + host: nextcloud.kube.huskypup.net + username: "" + password: "" + + phpConfigs: + upload.ini: | + upload_tmp_dir = /var/www/tmp + sys_temp_dir = /var/www/tmp + + extraEnv: + - name: TMPDIR + value: /var/www/tmp + - name: PHP_MEMORY_LIMIT + value: "2G" + - name: PHP_UPLOAD_LIMIT + value: "10G" + + configs: + proxy.config.php: |- + array( + 0 => '10.0.0.0/8', + 1 => '172.16.0.0/12', + ), + 'overwriteprotocol' => 'https', + 'overwrite.cli.url' => 'https://nextcloud.kube.huskypup.net', + 'allow_local_remote_servers' => true, + ); + temp.config.php: |- + '/var/www/tmp', + ); + + existingSecret: + enabled: true + secretName: nextcloud-admin-secret + usernameKey: username + passwordKey: password + + ingress: + enabled: false + +externalDatabase: + enabled: true + type: postgresql + host: pg-nextcloud-rw + port: 5432 + user: nextcloud + database: nextcloud + existingSecret: + enabled: true + secretName: pg-nextcloud-app + passwordKey: password + usernameKey: username + +persistence: + enabled: true + storageClass: local-path + accessMode: ReadWriteOnce + size: 30Gi + +resources: + requests: + cpu: 50m + memory: 256Mi + limits: + cpu: 500m + memory: 1Gi + +redis: + enabled: false + +externalRedis: + enabled: false + +metrics: + enabled: false