Compare commits

...
58 Commits
Author SHA1 Message Date
Ruben Hensen c384ae921f default: deny all, through a folder app of its own 2026-10-26 23:26:47 +01:00
Ruben Hensen 13b92fff54 dns: NetworkPolicy 2026-10-26 23:26:38 +01:00
Ruben Hensen 3834f7a354 mariadb-system: NetworkPolicy 2026-10-26 23:26:25 +01:00
Ruben Hensen 8110a235a9 velero: NetworkPolicy 2026-10-26 23:26:15 +01:00
Ruben Hensen fa33bf44ae external-secrets: NetworkPolicy, through a folder app of its own 2026-10-26 23:26:06 +01:00
Ruben Hensen d4e99d28c7 cnpg-system: NetworkPolicy, through a folder app of its own 2026-10-26 23:25:55 +01:00
Ruben Hensen c8a81e805d cert-manager: NetworkPolicy 2026-10-26 23:25:28 +01:00
Ruben Hensen 0dc69d34f0 Merge pull request #50 from rubenhensen/proxy-protocol
Pass the visitor's address from node001 to the home cluster
2026-10-26 23:12:14 +01:00
Ruben Hensen 30540ea579 node001: send the visitor's address to the home cluster
The stream server on 443 now starts each connection with a PROXY header.
nginx sends it to every upstream or none, so Stalwart (8443) and the
local nginx for Gatus and ntfy (8444) get theirs through a hop on
8445/8446 that takes it off again; nothing changes for them.

Deploy after the Gateway accepts the header, or the tunneled hosts stop
answering until it does.
2026-10-26 23:11:38 +01:00
Ruben Hensen 53d74768ad gateway: take the visitor's address from a PROXY header
node001 passes TLS through without opening it, so every request from
the tunneled hosts arrives from 46.224.26.65. Wordfence locked that
address out after 20 failed logins on 2026-10-26, and with it everyone
who logs in to scoutingmbg.nl. With a PROXY header from node001, Envoy
puts the visitor's address in X-Forwarded-For instead.

The header is optional: s2 and ntfy2 point straight at home, and
node001's proxy on port 80 sends none either.
2026-10-26 23:11:38 +01:00
Ruben Hensen b6462fb3a9 Merge pull request #49 from rubenhensen/roadmap-garage
docs: Velero on Garage
2026-10-26 23:04:15 +01:00
Ruben Hensen bed68c70a8 velero: the switch to Garage was on 10-26 2026-10-26 22:57:35 +01:00
Ruben Hensen 3991a8d726 docs: Velero on Garage, and the off-site copy from the bucket 2026-10-26 22:57:35 +01:00
Ruben Hensen 648f99c505 Merge pull request #35 from rubenhensen/velero-garage
Velero backs up to Garage instead of MinIO
2026-10-26 22:52:42 +01:00
Ruben Hensen 20bd3a4bca Merge pull request #39 from rubenhensen/immich-ml-v2
immich: the optimized ML models, cached on a volume
2026-10-25 17:48:40 +01:00
Ruben Hensen 6cba1a9363 immich: the optimized ML models, cached on a volume 2026-10-25 17:46:03 +01:00
Ruben Hensen acfce6645e velero: skip PVCs in Longhorn's no-backup group 2026-10-25 17:46:03 +01:00
Ruben Hensen 0de86dd65c Merge pull request #36 from rubenhensen/renovate-re2
Renovate: skip the non-app folders without a lookahead
2026-10-25 17:41:32 +01:00
Ruben Hensen d7ffdc05f6 docs: Renovate rejected the lookahead (#32) 2026-10-25 17:33:39 +01:00
Ruben Hensen c0f8034e14 renovate: skip the non-app folders through ignorePaths, not a lookahead 2026-10-25 17:33:39 +01:00
Ruben Hensen a15a4f7c75 Merge pull request #34 from rubenhensen/velero-garage-secret
Garage on the NAS, and Velero's key for it
2026-10-25 17:23:33 +01:00
Ruben Hensen bdafcaf1e0 backup-testing: read Velero's key for Garage 2026-10-25 17:10:32 +01:00
Ruben Hensen 03c3196242 velero: back up to Garage instead of MinIO 2026-10-25 17:10:32 +01:00
Ruben Hensen b072099cc1 nas: Garage keeps its metadata in SQLite 2026-10-25 17:08:13 +01:00
Ruben Hensen e10ca337f1 velero: a key for Garage, from Vault 2026-10-25 16:28:43 +01:00
Ruben Hensen c72b08867f nas: Garage, which takes over from MinIO 2026-10-25 16:28:43 +01:00
Ruben Hensen d85362ce3b docs: the restore test passed against the real backups 2026-10-25 14:57:18 +01:00
Ruben Hensen 018f528fb3 docs: a WordPress cache without plugins 2026-10-25 14:56:10 +01:00
Ruben Hensen 714712b862 docs: WordPress is capped at 12 workers 2026-10-25 13:11:20 +01:00
Ruben Hensen 7dc13466cb mbgwp: cap Apache at 12 workers so a crawler can't run it out of memory 2026-10-25 13:07:30 +01:00
Ruben Hensen 9b77596bcb backup-testing: take endpoint, region and bucket from Velero's location 2026-10-25 12:59:25 +01:00
Ruben Hensen fbf2cb2d92 docs: the unseal key is kept safe 2026-10-25 12:57:18 +01:00
Ruben Hensen 122873ca5c docs: hensen.io can expire 2026-10-25 12:54:32 +01:00
Ruben Hensen 71449dd185 docs: record the external-secrets upgrade 2026-10-25 12:54:01 +01:00
Ruben Hensen 297d2dffa5 Merge pull request #17 from rubenhensen/renovate/external-secrets-2.x
chore(deps): update helm chart external-secrets to v2.14.0
2026-10-25 12:50:28 +01:00
Ruben Hensen 46117ee658 chore(deps): update helm chart external-secrets to v2.14.0 2026-10-25 12:49:06 +01:00
Ruben Hensen 2a5bbc7f44 docs: the scoutingmbg.nl DNS questions are answered
Only the four records moved to node001, hensen.io is no longer needed, and SPF's a mechanism doesn't matter since all mail goes through Google.
2026-10-25 12:39:57 +01:00
Ruben Hensen d7b0372f1a docs: record the rest of the merged PRs 2026-10-25 12:37:25 +01:00
Ruben Hensen 0ad6ca816c docs: who cleaned up the snapshots, and the stalls per minute
Every minute with dqlite errors in the 23 hours sampled had the SSD fully busy; the controllers with 60s leases held.
2026-10-25 12:35:47 +01:00
Ruben Hensen 3e252799a4 Merge pull request #26 from rubenhensen/renovate/gitea-gitea-28.x
chore(deps): update gitea/gitea docker tag to v28
2026-10-25 12:25:03 +01:00
Ruben Hensen bfd5619e9e Merge pull request #27 from rubenhensen/renovate/github.com-canonical-go-dqlite-v3-3.x
fix(deps): update module github.com/canonical/go-dqlite/v3 to v3.0.4
2026-10-25 12:24:30 +01:00
Ruben Hensen 0b38d8dec8 Merge pull request #33 from rubenhensen/requests
Set resource requests from measured usage
2026-10-25 12:23:54 +01:00
Ruben Hensen 083353fb9d Merge pull request #23 from rubenhensen/renovate/binwiederhier-ntfy-2.x
chore(deps): update binwiederhier/ntfy docker tag to v2.34.0
2026-10-25 12:22:45 +01:00
Ruben Hensen 23f9c5c60a docs: record the merged Renovate PRs 2026-10-25 12:21:38 +01:00
Ruben Hensen a8bf3d5b64 Merge pull request #31 from rubenhensen/renovate/python-3.x
chore(deps): update python docker tag to v3.15.1
2026-10-25 12:17:31 +01:00
Ruben Hensen 618954ae2c Merge pull request #25 from rubenhensen/renovate/wordpress-0.x
chore(deps): update helm chart wordpress to v0.16.7
2026-10-25 12:17:17 +01:00
Ruben Hensen 44dc85d306 Merge pull request #24 from rubenhensen/renovate/https-github.com-kubernetes-csi-external-snapshotter.git-8.x
chore(deps): update dependency https://github.com/kubernetes-csi/external-snapshotter.git to v8.7.0
2026-10-25 12:16:25 +01:00
Ruben Hensen b60735f6b0 Merge pull request #28 from rubenhensen/renovate/cert-manager-1.21.x
chore(deps): update helm chart cert-manager to v1.21.3
2026-10-25 12:14:55 +01:00
Ruben Hensen 3a57e5be48 docs: WordPress went out of memory under a crawler 2026-10-25 09:45:33 +01:00
Ruben Hensen 1611f5d1ef docs: PR 33's window and Renovate's PR limit 2026-10-25 09:44:20 +01:00
Ruben Hensen 68b692a112 docs: requests are in PR 33 2026-10-25 07:10:37 +01:00
renovate[bot] 6b2f5855d7 chore(deps): update python docker tag to v3.15.1 2026-10-25 03:10:52 +00:00
renovate[bot] c25b8883da chore(deps): update gitea/gitea docker tag to v28 2026-10-25 03:06:44 +00:00
renovate[bot] 18f9e2efe6 chore(deps): update helm chart wordpress to v0.16.7 2026-10-25 03:06:35 +00:00
renovate[bot] 5f5f5b01f0 chore(deps): update dependency https://github.com/kubernetes-csi/external-snapshotter.git to v8.7.0 2026-10-25 03:06:27 +00:00
renovate[bot] 6206ab5a74 chore(deps): update binwiederhier/ntfy docker tag to v2.34.0 2026-10-25 03:06:23 +00:00
renovate[bot] 075b3686c3 chore(deps): update helm chart cert-manager to v1.21.3 2026-10-25 03:05:55 +00:00
renovate[bot] 45d1ea0d93 fix(deps): update module github.com/canonical/go-dqlite/v3 to v3.0.4 2026-10-23 20:04:51 +00:00
37 changed files with 811 additions and 116 deletions

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ spec:
project: default project: default
source: source:
repoURL: https://charts.jetstack.io repoURL: https://charts.jetstack.io
targetRevision: v1.21.2 targetRevision: v1.21.3
chart: cert-manager chart: cert-manager
helm: helm:
parameters: parameters:
+19
View File
@@ -0,0 +1,19 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cloudnative-pg-folder
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/rubenhensen/k8scd.git
targetRevision: HEAD
path: cloudnative-pg
destination:
server: https://kubernetes.default.svc
namespace: cnpg-system
syncPolicy:
automated:
selfHeal: true
+19
View File
@@ -0,0 +1,19 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: default-folder
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/rubenhensen/k8scd.git
targetRevision: HEAD
path: default
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
selfHeal: true
@@ -0,0 +1,19 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-secrets-folder
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/rubenhensen/k8scd.git
targetRevision: HEAD
path: external-secrets
destination:
server: https://kubernetes.default.svc
namespace: external-secrets
syncPolicy:
automated:
selfHeal: true
+1 -1
View File
@@ -17,7 +17,7 @@ spec:
sources: sources:
- chart: external-secrets - chart: external-secrets
repoURL: https://charts.external-secrets.io repoURL: https://charts.external-secrets.io
targetRevision: 2.13.0 targetRevision: 2.14.0
helm: helm:
values: | values: |
resources: resources:
+18 -1
View File
@@ -12,7 +12,7 @@ spec:
project: default project: default
source: source:
repoURL: https://groundhog2k.github.io/helm-charts/ repoURL: https://groundhog2k.github.io/helm-charts/
targetRevision: 0.15.0 targetRevision: 0.16.7
chart: wordpress chart: wordpress
helm: helm:
values: | values: |
@@ -101,6 +101,23 @@ spec:
timeoutSeconds: 5 timeoutSeconds: 5
failureThreshold: 30 failureThreshold: 30
# apache2.conf includes ports.conf after mods-enabled/*.conf, so the
# prefork settings here replace mpm_prefork.conf's MaxRequestWorkers
# 150. A worker takes ~45-60 MB (PSS), so 150 never fit in 1Gi: a
# crawler asking for 88 pages at once got the container OOMKilled on
# 2026-10-25. With 12 workers a burst waits in the listen backlog
# instead. Listen is the chart's default (containerPort).
apachePortsConfig: |
Listen 8000
<IfModule mpm_prefork_module>
StartServers 4
MinSpareServers 2
MaxSpareServers 6
ServerLimit 12
MaxRequestWorkers 12
MaxConnectionsPerChild 1000
</IfModule>
# Custom PHP configuration # Custom PHP configuration
customPhpConfig: | customPhpConfig: |
upload_max_filesize = 64M upload_max_filesize = 64M
+1 -1
View File
@@ -7,7 +7,7 @@ spec:
project: default project: default
source: source:
repoURL: https://github.com/kubernetes-csi/external-snapshotter.git repoURL: https://github.com/kubernetes-csi/external-snapshotter.git
targetRevision: v8.2.1 targetRevision: v8.7.0
path: deploy/kubernetes/snapshot-controller path: deploy/kubernetes/snapshot-controller
kustomize: kustomize:
patches: patches:
+16 -6
View File
@@ -3,7 +3,7 @@
# k3s in Docker, to prove the backups can be restored. Nothing on the NAS # k3s in Docker, to prove the backups can be restored. Nothing on the NAS
# changes: # changes:
# - one backup (backups/<name>/) and the kopia repositories of the chosen # - one backup (backups/<name>/) and the kopia repositories of the chosen
# namespaces (kopia/<namespace>/) are copied read-only from MinIO with # namespaces (kopia/<namespace>/) are copied read-only from the NAS with
# rclone into a Docker volume. A second Velero on the real bucket could # rclone into a Docker volume. A second Velero on the real bucket could
# run kopia maintenance on the same repositories as the cluster's own # run kopia maintenance on the same repositories as the cluster's own
# - `rclone serve s3` serves that copy to a Velero 1.18.2 in k3s, with a # - `rclone serve s3` serves that copy to a Velero 1.18.2 in k3s, with a
@@ -19,7 +19,7 @@
# Defaults: the newest Completed daily-backup, and ntfy freshrss # Defaults: the newest Completed daily-backup, and ntfy freshrss
# actual-budget. Not gatus: a restored Gatus would start sending alerts. # actual-budget. Not gatus: a restored Gatus would start sending alerts.
# #
# It reads Velero's MinIO key from the cluster (velero/cloud-credentials) # It reads Velero's S3 key from the cluster (velero/velero-s3-credentials)
# into a temporary directory, and the copied backup holds every Secret of # into a temporary directory, and the copied backup holds every Secret of
# the cluster; all of it is deleted at the end. KEEP=1 keeps the test # the cluster; all of it is deleted at the end. KEEP=1 keeps the test
# cluster (kubeconfig in the printed directory) until you run it with # cluster (kubeconfig in the printed directory) until you run it with
@@ -33,8 +33,6 @@
set -euo pipefail set -euo pipefail
PROD_KUBECONFIG=${PROD_KUBECONFIG:-$HOME/.kube/homelab-config} PROD_KUBECONFIG=${PROD_KUBECONFIG:-$HOME/.kube/homelab-config}
MINIO=${MINIO:-http://192.168.1.99:9000}
BUCKET=velero-backups
PORT=${PORT:-16443} PORT=${PORT:-16443}
K3S=rancher/k3s:v1.34.9-k3s1 K3S=rancher/k3s:v1.34.9-k3s1
RCLONE=rclone/rclone:1.75.1 RCLONE=rclone/rclone:1.75.1
@@ -73,6 +71,18 @@ wait_for() {
done done
} }
# Endpoint, region and bucket as Velero itself uses them. The NAS rejects a
# request signed for the wrong region (rclone defaults to us-east-1); MinIO
# did so with a bare 400 on HEAD.
if [[ -n ${FROM_VOLUME:-} ]]; then
BUCKET=${BUCKET:-velero-backups}
else
bsl=$(pk -n velero get backupstoragelocations default -o json)
S3URL=${S3URL:-$(jq -r '.spec.config.s3Url' <<<"$bsl")}
REGION=${REGION:-$(jq -r '.spec.config.region // "us-east-1"' <<<"$bsl")}
BUCKET=${BUCKET:-$(jq -r '.spec.objectStorage.bucket' <<<"$bsl")}
fi
backup=${1:-} backup=${1:-}
shift || true shift || true
namespaces=("$@") namespaces=("$@")
@@ -91,10 +101,10 @@ if [[ -n ${FROM_VOLUME:-} ]]; then
docker run --rm -v "$FROM_VOLUME:/from:ro" -v "$VOL:/data" alpine:3.22 cp -a /from/. /data/ docker run --rm -v "$FROM_VOLUME:/from:ro" -v "$VOL:/data" alpine:3.22 cp -a /from/. /data/
else else
echo "== copy from the NAS (read-only)" echo "== copy from the NAS (read-only)"
(umask 077; pk -n velero get secret cloud-credentials -o jsonpath='{.data.cloud}' | base64 -d | (umask 077; pk -n velero get secret velero-s3-credentials -o jsonpath='{.data.cloud}' | base64 -d |
awk -F' *= *' '/aws_access_key_id/ {print "RCLONE_CONFIG_NAS_ACCESS_KEY_ID="$2} awk -F' *= *' '/aws_access_key_id/ {print "RCLONE_CONFIG_NAS_ACCESS_KEY_ID="$2}
/aws_secret_access_key/ {print "RCLONE_CONFIG_NAS_SECRET_ACCESS_KEY="$2}' > "$WORK/nas.env" /aws_secret_access_key/ {print "RCLONE_CONFIG_NAS_SECRET_ACCESS_KEY="$2}' > "$WORK/nas.env"
printf 'RCLONE_CONFIG_NAS_TYPE=s3\nRCLONE_CONFIG_NAS_PROVIDER=Minio\nRCLONE_CONFIG_NAS_ENDPOINT=%s\n' "$MINIO" >> "$WORK/nas.env") printf 'RCLONE_CONFIG_NAS_TYPE=s3\nRCLONE_CONFIG_NAS_PROVIDER=Other\nRCLONE_CONFIG_NAS_ENDPOINT=%s\nRCLONE_CONFIG_NAS_REGION=%s\n' "$S3URL" "$REGION" >> "$WORK/nas.env")
copy() { docker run --rm --env-file "$WORK/nas.env" -v "$VOL:/data" "$RCLONE" copy --stats-one-line "nas:$BUCKET/$1" "/data/$BUCKET/$1"; } copy() { docker run --rm --env-file "$WORK/nas.env" -v "$VOL:/data" "$RCLONE" copy --stats-one-line "nas:$BUCKET/$1" "/data/$BUCKET/$1"; }
copy "backups/$backup" copy "backups/$backup"
for ns in "${namespaces[@]}"; do copy "kopia/$ns"; done for ns in "${namespaces[@]}"; do copy "kopia/$ns"; done
+73
View File
@@ -0,0 +1,73 @@
# cert-manager's three pods reach each other, DNS and the API server (the
# node, port 16443), and take connections only from each other and the
# node: the API server calls the webhook from there and the kubelet probes
# come from there too (10.1.7.64 is the node's Calico VXLAN address,
# projectcalico.org/IPv4VXLANTunnelAddr). Nothing scrapes the metrics on
# 9402. The HTTP-01 solver pods run in each Certificate's own namespace
# (acme-http01-solver in <namespace>/networkpolicy.yaml), not here.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- from:
- ipBlock:
cidr: 192.168.1.204/32
- ipBlock:
cidr: 10.1.7.64/32
egress:
- to:
- podSelector: {}
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 192.168.1.204/32
ports:
- port: 16443
---
# The controller talks to Let's Encrypt (443), and before it lets Let's
# Encrypt check a challenge it fetches http://<host>/.well-known/... itself
# (80). Every certificate's host has a public address, node001's or the
# home router's (checked 2026-10-26), so the LAN stays out.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: controller-to-internet
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: cert-manager
app.kubernetes.io/component: controller
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 100.64.0.0/10
- 169.254.0.0/16
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- port: 80
- port: 443
+56
View File
@@ -0,0 +1,56 @@
# The CloudNativePG operator reaches DNS, the API server (the node, port
# 16443) and the PostgreSQL instances it runs in other namespaces: the
# instance manager's status port (8000) and PostgreSQL (5432), which the
# instances' own policies let in (postgres-cnpg in authentik and immich).
# Besides its own namespace it takes connections only from the node: the
# API server calls its webhook (9443) from there and the kubelet probes
# come from there too (10.1.7.64 is the node's Calico VXLAN address,
# projectcalico.org/IPv4VXLANTunnelAddr). The chart's Application
# (apps/templates/cloudnativepg-helm.yaml) only installs the chart, so
# this folder gets an Application of its own, cloudnative-pg-folder.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- from:
- ipBlock:
cidr: 192.168.1.204/32
- ipBlock:
cidr: 10.1.7.64/32
egress:
- to:
- podSelector: {}
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 192.168.1.204/32
ports:
- port: 16443
- to:
- namespaceSelector: {}
podSelector:
matchExpressions:
- key: cnpg.io/cluster
operator: Exists
ports:
- port: 8000
- port: 5432
+14
View File
@@ -0,0 +1,14 @@
# Nothing runs in default (2026-10-26: only the kubernetes Service, whose
# endpoint is the API server on the node, which this doesn't touch). A pod
# started here, by `kubectl run` or a manifest without a namespace, gets
# no network at all, instead of reaching the LAN, Vault and every app.
# Anything that needs a network gets a namespace and a policy of its own.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
+1
View File
@@ -7,6 +7,7 @@ resources:
- external-secret.yaml - external-secret.yaml
- rbac.yaml - rbac.yaml
- cronjob.yaml - cronjob.yaml
- networkpolicy.yaml
configMapGenerator: configMapGenerator:
- name: dns-sync-script - name: dns-sync-script
+37
View File
@@ -0,0 +1,37 @@
# The only pod here is the dns-sync Job (a PostSync hook): it installs its
# Python packages from PyPI and pushes domains/ to TransIP's API, so it
# reaches DNS and the internet (not the LAN, the API server or other
# namespaces). Nothing connects to it.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 100.64.0.0/10
- 169.254.0.0/16
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- port: 443
+7 -9
View File
@@ -123,15 +123,13 @@ D. auth.scoutingmbg.nl (2026-10-25)
## Open points ## Open points
- DNS (2026-10-24): the group's DNS host (ns1/ns2.webhosted.nl) changed - DNS (2026-10-24/25): the group's DNS host (ns1/ns2.webhosted.nl) pointed
foto.scoutingmbg.nl from a CNAME to `mbg.hensen.io` (the home IP) to a scoutingmbg.nl, www, foto and auth at node001 (rubenhensen.nl); those are
CNAME to `rubenhensen.nl`, which is node001 on Hetzner. Only ns2 has it all the records it moved, and node001 tunnels all four home. ns1 lagged
(serial 2026102471); ns1 is stuck on serial 2026060640 from June and behind ns2 (a zone serial from June) until 2026-10-24 16:50 UTC. Nothing
still answers `mbg.hensen.io`. node001 now tunnels foto.scoutingmbg.nl to uses mbg.hensen.io anymore, so hensen.io can expire. The SPF record's
the home cluster like its other hosts, so both answers work. Keep `a` now means node001, which is fine: the WordPress site sends no mail
hensen.io until ns1 has caught up; ask the DNS host to fix ns1, and and the group's mail goes through Google (`mx`)
decide whether foto should stay behind node001 or point straight home
(CNAME `scoutingmbg.nl`)
- Public shared links become public for real once sol-auth is gone. - Public shared links become public for real once sol-auth is gone.
- Photos leiding back up from their phones stay private until they add - Photos leiding back up from their phones stay private until they add
them to a shared album. them to a shared album.
+3 -2
View File
@@ -38,14 +38,15 @@ between them live.
## Before: shrink what has to move ## Before: shrink what has to move
- [ ] Delete stale snapshots. 188 snapshots hold 334 GiB, 214 GiB of it in - [x] Delete stale snapshots. 188 snapshots hold 334 GiB, 214 GiB of it in
snapshots from before October 2026, nearly all on immich-claim snapshots from before October 2026, nearly all on immich-claim
(pvc-75d269dd): a 72.5 GiB one from 2024-12-20, 71.3 GiB from (pvc-75d269dd): a 72.5 GiB one from 2024-12-20, 71.3 GiB from
2025-03-02, daily ones from 2025-03-13 to 03-20 (a recurring job that no 2025-03-02, daily ones from 2025-03-13 to 03-20 (a recurring job that no
longer exists), 56.6 GiB from 2026-01-20. The Longhorn backups on the NAS longer exists), 56.6 GiB from 2026-01-20. The Longhorn backups on the NAS
don't depend on them. Deleting a snapshot merges it into the next one, don't depend on them. Deleting a snapshot merges it into the next one,
which writes a lot, so do this in a quiet hour, one volume at a time. which writes a lot, so do this in a quiet hour, one volume at a time.
Happened on 2026-10-24 (who started it isn't in the logs): immich-claim's Happened on 2026-10-24, from a Claude Code session at Ruben's request
(the 45 snapshots from before October 2026, small volumes first): immich-claim's
old snapshots were removed at 14:43 UTC and Longhorn merged them one old snapshots were removed at 14:43 UTC and Longhorn merged them one
after another, about 45 minutes each, until 23:15. For those 8.5 hours after another, about 45 minutes each, until 23:15. For those 8.5 hours
sda ran 250-440 writes/s and 200-400 reads/s at 6-20 ms per write on sda ran 250-440 writes/s and 200-400 reads/s at 6-20 ms per write on
+139 -64
View File
@@ -10,10 +10,11 @@ One node: opti1, an OptiPlex 5060 with an i5-8600T, 32 GB and one 1 TB
SATA SSD that is 72% full. microk8s v1.36.5 from 1.36/edge, snap held. SATA SSD that is 72% full. microk8s v1.36.5 from 1.36/edge, snap held.
35 Argo CD apps (with metrics-server), all Synced/Healthy; every Longhorn 35 Argo CD apps (with metrics-server), all Synced/Healthy; every Longhorn
volume has one replica. authentik 2026.8.5, Vault 2.2.1. Backups go to the volume has one replica. authentik 2026.8.5, Vault 2.2.1. Backups go to the
NAS (192.168.1.99): Longhorn to NFS at 00:00 (7 kept), Velero to MinIO at NAS (192.168.1.99): Longhorn to NFS at 00:00 (7 kept), Velero to Garage
01:00 (daily kept 30 days, weekly 90 days). Gatus on s1 (node001, Hetzner) (MinIO until 2026-10-26) at 01:00 (daily kept 30 days, weekly 90 days).
and s2 (home) watch each other and alert through ntfy; s2 also gets an Gatus on s1 (node001, Hetzner) and s2 (home) watch each other and alert
hourly report on the backups and Vault's seal from inside the cluster. through ntfy; s2 also gets an hourly report from inside the cluster on the
backups, Vault's seal, the Argo CD apps and the CNPG clusters.
Every app namespace has NetworkPolicies, and CI renders and validates all Every app namespace has NetworkPolicies, and CI renders and validates all
apps on every PR. node001 runs mail and tunnels the public hosts home. apps on every PR. node001 runs mail and tunnels the public hosts home.
@@ -33,8 +34,9 @@ docs/longhorn-second-ssd.md:
- [ ] The upgrade's dqlite and backend copies in `/root/` on opti1 - [ ] The upgrade's dqlite and backend copies in `/root/` on opti1
(`backend-*.tar`, `dqlite-pre-*`) (`backend-*.tar`, `dqlite-pre-*`)
- [ ] Log in to immich as a jeugdlid (quota 0) - [ ] Log in to immich as a jeugdlid (quota 0)
- [ ] Back to 1.36/stable once it is at v1.36.5 or later. The snap is held, - [ ] Back to 1.36/stable once it is at v1.36.5 or later (still v1.36.2 on
so microk8s gets no updates at all until someone refreshes it 2026-10-25). The snap is held, so microk8s gets no updates at all until
someone refreshes it
- [ ] Second SSD, docs/longhorn-second-ssd.md - [ ] Second SSD, docs/longhorn-second-ssd.md
- [ ] Optional: `MACHINE_LEARNING_MODEL_REVISION=v2` for immich ML - [ ] Optional: `MACHINE_LEARNING_MODEL_REVISION=v2` for immich ML
@@ -48,8 +50,9 @@ group's ns2: ns1 is stuck on an old serial and still answers
mbg.hensen.io (docs/immich-scouts-online.md). mbg.hensen.io (docs/immich-scouts-online.md).
- [x] Expiry date: 2027-01-09 (whois, 2026-10-25; registrar Key-Systems) - [x] Expiry date: 2027-01-09 (whois, 2026-10-25; registrar Key-Systems)
- [ ] Have the group's DNS host fix ns1; keep hensen.io until it answers - [x] ns1 caught up on 2026-10-24 16:50 UTC; both ns1 and ns2 answer
rubenhensen.nl too `rubenhensen.nl` for foto (checked 2026-10-25), and nothing in the
cluster uses hensen.io anymore. hensen.io can expire
### 2. Off-site backups ### 2. Off-site backups
@@ -60,24 +63,25 @@ belong to the group.
There is no off-site copy (2026-10-25). The plan: swap S3 buckets with a There is no off-site copy (2026-10-25). The plan: swap S3 buckets with a
friend. friend.
- [ ] Ours to his bucket: Hyper Backup on the Synology, destination "S3 - [ ] Ours to his bucket: `rclone sync` from Garage's `velero-backups`
Storage" with his endpoint as a custom server. Client-side encryption (~190 GB) to his bucket through an rclone `crypt` remote, in Docker on
on: the kopia repositories use Velero's default password the NAS like the copy from MinIO (see 5). Encrypted on our side: the
(`static-passw0rd`), so without it he could read everything, the kopia repositories use Velero's default password (`static-passw0rd`),
group's photos included. Keep the encryption key next to the Vault so without it he could read everything, the group's photos included.
unseal key. Source: the shared folder with MinIO's `velero-backups` Keep the crypt passwords next to the Vault unseal key. Daily after the
bucket (~150-200 GB). Daily after the Velero backup, Smart Recycle, Velero backup, ntfy on failure. Hyper Backup, the earlier plan, backs up
mail on failure shared folders; Garage keeps its objects as blocks plus a metadata
database, and a copy of that folder taken while Garage runs is not
consistent
- [ ] Needed from him: endpoint, which S3 server he runs, space (~200 GB - [ ] Needed from him: endpoint, which S3 server he runs, space (~200 GB
now, 500 GB to grow), and versioning or object lock on the bucket, so now, 500 GB to grow), and versioning or object lock on the bucket, so
whoever takes over the NAS can't delete the off-site copy as well whoever takes over the NAS can't delete the off-site copy as well
- [ ] His backups with us: Garage (not MinIO, see 5) in Container Manager - [ ] His backups with us: a bucket and key for him, with a quota, in the
on the Synology, in its own shared folder, with a bucket and key for him Garage on the NAS (`nas/garage`, see 5). Only the S3 API public, as
and a quota. Only the S3 API public, as `s3.rubenhensen.nl` through `s3.rubenhensen.nl` through node001 and Envoy with a cert-manager
node001 and Envoy with a cert-manager certificate; the admin API stays certificate; the admin API stays on the LAN. The cluster side (DNS,
on the LAN. The cluster side (DNS, node001 tunnel, Certificate, node001 tunnel, Certificate, HTTPRoute) goes in git. Needed: how much
HTTPRoute) goes in git. Needed: how much space he wants, free space on space he wants, free space on the NAS
the NAS
- [ ] Decide which copy of the immich library goes off-site (see 5) - [ ] Decide which copy of the immich library goes off-site (see 5)
- [ ] A restore test that runs on a schedule. `backup-testing/` predates - [ ] A restore test that runs on a schedule. `backup-testing/` predates
Envoy Gateway and still rewrites `*.hensen.io` and bypasses sol-auth. Envoy Gateway and still rewrites `*.hensen.io` and bypasses sol-auth.
@@ -97,8 +101,20 @@ friend.
ReadOnly location: data restored byte for byte (same sha256), deployment ReadOnly location: data restored byte for byte (same sha256), deployment
available, everything removed afterwards. Two pitfalls found on the way available, everything removed afterwards. Two pitfalls found on the way
are in the script's header. Not yet run against the real backups: are in the script's header. Not yet run against the real backups:
- [ ] Run `backup-testing/restore-test.sh` (it reads the MinIO key from - [x] Ran `backup-testing/restore-test.sh` on 2026-10-25 against
the cluster), then schedule it daily-backup-20261025010027 (ntfy, freshrss, actual-budget): every pod
volume came back with its backed-up byte count (actual-budget 8.4 MB,
freshrss 293 MB, ntfy 180 KB) and all deployments became available. The
first run failed on a 400 from MinIO: rclone signed for us-east-1 while
MinIO runs region `minio`; the script now reads endpoint, region and
bucket from Velero's location. Copying freshrss's kopia repository took
14 of the ~17 minutes
- [x] Ran it again on 2026-10-26 against the copy in Garage
(daily-backup-20261026010028, same namespaces), before Velero switched
over (see 5): passed, in 5 minutes instead of 17
- [ ] Run it on a schedule. It needs Docker and an S3 key, so not in the
cluster itself: the Mac (launchd) or node001, ideally with a read-only
Garage key (`garage key create`, `garage bucket allow --read`)
### 3. authentik 2026.2.1 → 2026.8.5 ### 3. authentik 2026.2.1 → 2026.8.5
@@ -153,8 +169,8 @@ unseals it.
- The OIDC job called `vault auth tune -listing_visibility`, a flag that - The OIDC job called `vault auth tune -listing_visibility`, a flag that
doesn't exist, so the OIDC mount's listing visibility was never set. doesn't exist, so the OIDC mount's listing visibility was never set.
Fixed; the job now uses the 2.2.1 CLI instead of 1.15 Fixed; the job now uses the 2.2.1 CLI instead of 1.15
- [ ] There is one unseal key (1 share, threshold 1). Keep it in two - [x] There is one unseal key (1 share, threshold 1), kept safe outside
places outside this cluster; without it the data in Vault is lost this cluster (2026-10-25); without it the data in Vault is lost
- [x] Unsealing is in the SSD install steps - [x] Unsealing is in the SSD install steps
- [ ] After a week without problems: delete the three snapshots - [ ] After a week without problems: delete the three snapshots
@@ -181,7 +197,7 @@ Findings from 2026-10-25:
- Velero copies the CNPG and MariaDB data directories file by file while - Velero copies the CNPG and MariaDB data directories file by file while
the databases run, which is not a consistent copy. The Longhorn backups the databases run, which is not a consistent copy. The Longhorn backups
start from a snapshot and are crash-consistent. CNPG can back up to start from a snapshot and are crash-consistent. CNPG can back up to
MinIO itself (barman cloud plugin), with point-in-time recovery. Garage itself (barman cloud plugin), with point-in-time recovery.
- The PartiallyFailed daily backup of 10-23 (emptyDirs of Velero's own - The PartiallyFailed daily backup of 10-23 (emptyDirs of Velero's own
maintenance job pods) was fixed that afternoon by maintenance job pods) was fixed that afternoon by
`velero/skip-emptydir-policy.yaml`. `velero/skip-emptydir-policy.yaml`.
@@ -189,10 +205,31 @@ Findings from 2026-10-25:
- [ ] Whether the immich library stays in Velero. It stays for now - [ ] Whether the immich library stays in Velero. It stays for now
(2026-10-25); decide again with the off-site copy (2) (2026-10-25); decide again with the off-site copy (2)
- [ ] CNPG backups through the barman cloud plugin - [ ] CNPG backups through the barman cloud plugin
- [ ] Velero's target, MinIO on the NAS, is no longer maintained: no - [x] Velero's target, MinIO on the NAS, is no longer maintained: no
community images since October 2025, maintenance mode since December community images since October 2025, maintenance mode since December
2025, and the repository is archived. Move Velero to the Garage from 2 2025, and the repository is archived. Since 2026-10-26 Velero backs up
and switch MinIO off to Garage on the same NAS (`nas/garage`, PR #35). The bucket was copied
with rclone (185 GiB in about 6 hours, then a catch-up sync just before
the switch), so the existing backups and kopia repositories carried on:
after the switch all 55 backups were still listed, and a manual backup
of ntfy found the existing repository and used the night's snapshot as
its parent. Two things made the NAS slow during the copy: Garage on
LMDB with `metadata_fsync`, ~270 disk writes/s while idle (now SQLite,
see `nas/garage/garage.toml`), and MinIO's scanner, which kept one disk
at 97% busy. The scanner is now at `slowest` (`mc admin config set local
scanner speed=slowest` in the MinIO container); it may have slowed
Velero's backups to MinIO as well
- [ ] Switch MinIO off once Velero has run on Garage for a week (from
2026-11-02): stop the container, then delete `/volume1/minio` (343 GiB)
and the hand-made Secret `velero/cloud-credentials` (MinIO's key). Only
MinIO still has the kopia repositories of namespaces that are gone
(~156 GB: nextcloud, ttrss, whisper-priv, whisper-webui, blog, ghost,
mbg-wordpress, sogo, sogo-mail, outrun-game, owntracks); decide first
whether any of them is worth keeping
- [ ] Garage's SQLite WAL (`meta/db.sqlite-wal`) was 1.7 GB on
2026-10-26 next to a 270 MB database, and going by the folders in
`meta/snapshots`, each 6-hourly metadata snapshot takes about 70
minutes. Not growing; check again after a week of normal use
### 6. Monitoring ### 6. Monitoring
@@ -265,8 +302,42 @@ Counted 2026-10-25 over the 73 running containers:
external-secrets, envoy-gateway-system, velero, mariadb-system, dns, external-secrets, envoy-gateway-system, velero, mariadb-system, dns,
default) still have none; `default` can reach the LAN and everything default) still have none; `default` can reach the LAN and everything
without a policy without a policy
- [ ] Requests and memory limits, the big ones first (immich, authentik, - [x] Requests: PR #33, merged 2026-10-25 11:23 UTC, for everything that
Argo CD's repo-server) had none except the Vault server, from `kubectl top` every 5 minutes
from 03:13 to 08:40 UTC (highest memory plus 10%, CPU at p95, 10m
floor). Rolled out in about eight minutes; both CNPG instances
restarted and came back healthy, the OIDC endpoints, the logins and
immich answered, no OOM kills. The node now has 3770m CPU (62%, of which
1440m is Longhorn's two instance managers) and 9158Mi memory (28%)
requested. Still without requests: Longhorn (22 containers, its chart),
the Vault servers, the LDAP outpost, immich's PG14 cluster and three in
kube-system
- [ ] Memory limits, once there are daytime numbers: the night ones miss
immich's machine learning at work (it idles at 234Mi)
- [x] WordPress (scoutingmbg.nl) was OOMKilled at 08:27:59 UTC on
2026-10-25, its third restart: a crawler asked for 88 pages and assets
within a few seconds (normally ~6 a minute), Apache started a PHP worker
for each (~45-60 MB each, `MaxRequestWorkers` was the default 150), and
the container went past its 1Gi limit. Unrelated to the
NetworkPolicies. Now capped at 12 workers through the chart's
`apachePortsConfig` (ports.conf comes after mods-enabled/*.conf), checked
with `apache2ctl -t` in the same image first. Replaying the burst (90
page requests, 40 at a time): all 200, at most 13 Apache processes and
572Mi, no restart; the price is waiting (4.3s median, up to ~22s at the
peak) instead of a crash
- [ ] Optional, no plugins wanted on WordPress: a small nginx cache in the
cluster between Envoy and WordPress that keeps anonymous GETs for a few
minutes and passes anything with a WordPress cookie through. A page
renders in 0.4-0.7s when the site is quiet and OPcache is on, so the
waiting in a burst is queueing; the cap alone already prevents the crash
- [ ] Fewer plugins on the site (17 now; up to whoever runs it):
`litespeed-cache` only caches pages on a LiteSpeed server, so it does
little here, `wordpress-importer` is usually for a one-off import, and
`code-snippets` runs PHP from the dashboard
- [ ] Rate limiting per visitor in Envoy needs the client's address, which
node001 loses: it passes TLS through, so every request arrives from
46.224.26.65. That needs the PROXY protocol from node001's nginx to Envoy
(ClientTrafficPolicy `enableProxyProtocol`)
- [x] Probes for gitea (`/api/healthz`), freshrss (TCP, since - [x] Probes for gitea (`/api/healthz`), freshrss (TCP, since
mod_auth_openidc answers 401 to non-browsers) and serpbear (`/login`) mod_auth_openidc answers 401 to non-browsers) and serpbear (`/login`)
(2026-10-25). Liveness only after 5 failures 30s apart, to ride out the (2026-10-25). Liveness only after 5 failures 30s apart, to ride out the
@@ -277,8 +348,10 @@ Counted 2026-10-25 over the 73 running containers:
### 8. Renovate and CI ### 8. Renovate and CI
10 Renovate PRs are open, the oldest from February, and nothing checks a On 2026-10-25 10 Renovate PRs were open, the oldest from February, and
PR before Argo CD applies it. nothing checked a PR before Argo CD applied it. All of them have since been
merged or closed (below), none are open (checked 2026-10-25), and CI checks
every PR.
- [x] `.github/workflows/validate.yaml` runs `tools/ci/validate.py` on - [x] `.github/workflows/validate.yaml` runs `tools/ci/validate.py` on
every PR and push (2026-10-25): it renders all 36 apps the way Argo CD every PR and push (2026-10-25): it renders all 36 apps the way Argo CD
@@ -314,34 +387,30 @@ PR before Argo CD applies it.
cover every app folder, a regex manager reads `imageName` in cover every app folder, a regex manager reads `imageName` in
`*/postgresql-cluster*.yaml`, and major PostgreSQL updates are off `*/postgresql-cluster*.yaml`, and major PostgreSQL updates are off
(those need pg_upgrade or a dump). Expect a batch of new PRs (those need pg_upgrade or a dump). Expect a batch of new PRs
(2026-10-25; `renovate-config-validator` passes) (2026-10-25). They only come once fewer than 10 Renovate PRs are open
- [ ] The open PRs, all validated on 2026-10-25 against main as it is now (`config:recommended`'s `prConcurrentLimit`); until then they wait
(rebases requested, so the checks show on the PRs once Renovate has under "Rate-Limited" on the Dependency Dashboard (#2)
pushed). Merging is yours; merging deploys: - The other folders were left out with a `(?!…)` lookahead. Renovate
- #28 cert-manager v1.21.3: security release, "All users should compiles these patterns with RE2, which has no lookahead, so it
upgrade" (drops the compromised eMudhra root, fixes an ARI renewal rejected the config and stopped opening PRs (#32). Each manager now
loop and a private key copy path). Merge first takes every folder and skips the others through its own
- #24 external-snapshotter v8.2.1 → v8.7.0: 8.6 moved VolumeGroupSnapshot `ignorePaths`. Renovate 44.181.6 with RE2 loaded fails the old config
to v1 and 8.7 turns its feature gate on; the cluster already serves the with #32's error and passes the new one, and its matcher picks the
v1 CRDs. Merge; close #15 same 166 files as the old pattern did
- #25 WordPress chart 0.15.0 → 0.16.7: the image is pinned - [x] Merged 2026-10-25 and checked after the sync: #28 cert-manager
(7.1.3-php8.3-apache), so the render only changes labels and v1.21.3 (security release; all three pods on v1.21.3), #24
checksums (a pod restart). Merge; #19 (0.15.1) is superseded external-snapshotter v8.7.0 (its manifests ship the v8.6.0 image, as
- #26 Gitea 1.27.3 → 28.2.0 (Gitea renumbered after 1.27): security v8.2.1 shipped v8.2.0; both replicas up, no errors), #25 WordPress chart
fixes; the breaking changes are to egress host lists 0.16.7 (image unchanged, site 200), #31 python 3.15.1 (the cluster check
(`ALLOWED_DOMAINS` and friends), none of which are set here. Database ran clean on it). Closed: #19, #29, and #15 by Renovate. Later the same
migrations can't be undone, so merge after a fresh backup day: #23 ntfy v2.34.0, #27 go-dqlite v3.0.4, #33 (see 7) and #26 Gitea
- #23 ntfy v2.28.0 → v2.34.0: six releases in 17 days; 2.31.0 had a 28.2.0 (migrations up to 357 ran clean, `/api/healthz` 200; no backup of
database load bug that ntfy.sh rolled back (fixed in 2.31.1). No its own beforehand, the nightly ones are the way back)
breaking migrations. Fine, or wait a few days for 2.34 to settle - [x] #17 external-secrets 2.14.0, merged 2026-10-25 11:50 UTC after a
- #17 external-secrets 2.13.0 → 2.14.0: no breaking changes in the manual rebase (#33 had added values on the next line, which git counts
notes; every secret goes through it, so merge when there's time to as a conflict). All three pods on v2.14.0, the store Ready, and 24 of 25
watch a force-sync ExternalSecrets refreshed on a force-sync; the 25th is the generated
- #27 go-dqlite v3.0.4 for `tools/dqlite-vacuum`: builds; harmless `gatus/backup-check-token`, which never refreshes by design
- #29 yaml.v3 for the same tool: builds, but the code imports yaml.v2,
so it only adds an unused requirement. Close
- #31 python 3.15.1 for `gatus/backup-check.yaml`: the check's tests
(made-up data for every failure) pass on that image. Merge
### 9. One node ### 9. One node
@@ -357,6 +426,12 @@ step after that, if downtime starts to matter.
`backup-testing/test-velero-backup.sh.bak` (an older copy); the 15 `backup-testing/test-velero-backup.sh.bak` (an older copy); the 15
completed kopia maintenance jobs of repositories that no longer exist completed kopia maintenance jobs of repositories that no longer exist
(nextcloud, owntracks, sogo, sogo-mail, ttrss) (nextcloud, owntracks, sogo, sogo-mail, ttrss)
- [ ] freshrss mounts `freshrss-claim2` and `freshrss-claim3` as whole
volumes over the file paths `data/config.custom.php` and
`data/config-user.custom.php`, so FreshRSS sees two empty directories
there and the volumes hold nothing (0 bytes in every backup). Drop both
mounts and the two PVCs, or make them ConfigMap files if custom config
is wanted
- [ ] `kubectl delete namespace secrets`, empty since its app went - [ ] `kubectl delete namespace secrets`, empty since its app went
- [ ] 41 Failed Velero backups: the dailies from 2026-09-26 to 10-21 and - [ ] 41 Failed Velero backups: the dailies from 2026-09-26 to 10-21 and
the weeklies since 08-02. Velero doesn't restore from a Failed backup; the weeklies since 08-02. Velero doesn't restore from a Failed backup;
@@ -398,7 +473,7 @@ critical / high:
| tensorchord/cloudnative-vectorchord:17.11-1.1.1 | 17 | 136 | immich's database; look for a newer tag | | tensorchord/cloudnative-vectorchord:17.11-1.1.1 | 17 | 136 | immich's database; look for a newer tag |
| longhornio/longhorn-instance-manager:v1.7.3 | 11 | 159 | still serves 16 volumes, see below | | longhornio/longhorn-instance-manager:v1.7.3 | 11 | 159 | still serves 16 volumes, see below |
| actualbudget/actual-server:26.6.0 | 8 | 83 | chart 1.8.9; Renovate now covers it | | actualbudget/actual-server:26.6.0 | 8 | 83 | chart 1.8.9; Renovate now covers it |
| gitea/gitea:1.27.3 | 8 | 43 | PR #26 (28.2.0) | | gitea/gitea:1.27.3 | 8 | 43 | now 28.2.0 (PR #26, 2026-10-25); not scanned again |
| immich-server / machine-learning v3.3.5 | 7, 7 | 113, 78 | a newer immich release | | immich-server / machine-learning v3.3.5 | 7, 7 | 113, 78 | a newer immich release |
| towfiqi/serpbear:3.1.0 | 6 | 77 | a newer release | | towfiqi/serpbear:3.1.0 | 6 | 77 | a newer release |
| rubenhensen/bolletjestrui:main-10268f1 | 5 | 76 | rebuild on a current base image | | rubenhensen/bolletjestrui:main-10268f1 | 5 | 76 | rebuild on a current base image |
+7
View File
@@ -282,6 +282,13 @@ whatever time of day the hourly schedule had drifted to.
migrations): 19-33 ms per write, single 2s windows near 2 s per write, migrations): 19-33 ms per write, single 2s windows near 2 s per write,
pressure 78%. The CSI sidecars and Longhorn's global manager lost pressure 78%. The CSI sidecars and Longhorn's global manager lost
their leases at 01:31 their leases at 01:31
Per minute against dqlite's own errors (2026-10-24 12:35 to 2026-10-25
11:30, leaving out the CNPG range cancels): all 67 minutes with errors
had sda ~100% busy, single 2s windows at 200 ms to 2.2 s per write and
"full" pressure at 45-78%, against a median minute peak of 24 ms. The
worst minute after the merges was the rollout of eight merged PRs at
2026-10-25 11:25-11:27 (77 errors in one minute). The snapshot-controller,
CNPG and Envoy Gateway held their 60s leases through all of it
Ubuntu's sysstat already keeps per-device history every 10 minutes Ubuntu's sysstat already keeps per-device history every 10 minutes
without a script: `sar -d -f /var/log/sysstat/sa24` showed sda at without a script: `sar -d -f /var/log/sysstat/sa24` showed sda at
~26 MB/s read and ~27 MB/s written, 9 ms await, during the merging ~26 MB/s read and ~27 MB/s written, 9 ms await, during the merging
+68
View File
@@ -0,0 +1,68 @@
# External Secrets' three pods reach each other, DNS and the API server
# (the node, port 16443); the controller also reads Vault
# (vault-active.vault.svc:8200, which vault/networkpolicy.yaml lets it
# into). Besides their own namespace they take connections only from the
# node: the API server calls the webhook from there and the kubelet probes
# come from there too (10.1.7.64 is the node's Calico VXLAN address,
# projectcalico.org/IPv4VXLANTunnelAddr). The Password generator behind
# gatus/backup-check-token needs no network. The chart's Application
# (apps/templates/external-secrets-helm.yaml) only installs the chart, so
# this folder gets an Application of its own, external-secrets-folder.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- from:
- ipBlock:
cidr: 192.168.1.204/32
- ipBlock:
cidr: 10.1.7.64/32
egress:
- to:
- podSelector: {}
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 192.168.1.204/32
ports:
- port: 16443
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: controller-to-vault
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: external-secrets
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: vault
podSelector:
matchLabels:
app.kubernetes.io/name: vault
ports:
- port: 8200
+23
View File
@@ -0,0 +1,23 @@
# node001 passes TLS for the tunneled hosts through without opening it, so
# without this every request arrives from 46.224.26.65. Wordfence locks that
# address out after 20 failed logins (2026-10-26), and with it everyone who
# logs in to scoutingmbg.nl. node001 puts a PROXY header with the visitor's
# address in front of each connection; Envoy takes the address from there
# and adds it to X-Forwarded-For.
#
# Optional, because s2 and ntfy2 point straight at home and their clients
# send no header, and neither does node001's proxy on port 80. The price:
# whoever connects to the home address directly can send a header of their
# own and claim any address.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: proxy-protocol
namespace: envoy-gateway-system
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: homelab
proxyProtocol:
optional: true
+1 -1
View File
@@ -256,7 +256,7 @@ spec:
type: RuntimeDefault type: RuntimeDefault
containers: containers:
- name: check - name: check
image: python:3.13.16-alpine3.24 image: python:3.15.1-alpine3.24
command: ["python", "/script/check.py"] command: ["python", "/script/check.py"]
env: env:
- name: PYTHONDONTWRITEBYTECODE - name: PYTHONDONTWRITEBYTECODE
+1 -1
View File
@@ -20,7 +20,7 @@ spec:
fsGroup: 1000 fsGroup: 1000
containers: containers:
- name: gitea - name: gitea
image: gitea/gitea:1.27.3 image: gitea/gitea:28.2.0
env: env:
- name: USER_UID - name: USER_UID
value: "1000" value: "1000"
+14 -3
View File
@@ -127,6 +127,9 @@ spec:
TRANSFORMERS_CACHE: /cache TRANSFORMERS_CACHE: /cache
HF_XET_CACHE: /cache/huggingface-xet HF_XET_CACHE: /cache/huggingface-xet
MPLCONFIGDIR: /cache/matplotlib-config MPLCONFIGDIR: /cache/matplotlib-config
# The optimized models of v3.3.0: faster, less memory,
# and the same results, so no jobs to run again.
MACHINE_LEARNING_MODEL_REVISION: v2
# Trust store without the compromised eMudhra # Trust store without the compromised eMudhra
# intermediates; requests would otherwise use certifi. # intermediates; requests would otherwise use certifi.
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
@@ -139,11 +142,19 @@ spec:
globalMounts: globalMounts:
- path: /etc/ssl/certs - path: /etc/ssl/certs
readOnly: true readOnly: true
# The models and their one-off preparation, kept across restarts.
# Downloaded again if lost, so no backups: the no-backup group
# keeps it out of Longhorn's default one (backup-daily), and
# Velero skips that label too (velero/skip-emptydir-policy.yaml).
cache: cache:
enabled: true enabled: true
# Optional: to avoid downloading the ML models every start, use type: persistentVolumeClaim
# a persistentVolumeClaim with size, accessMode and storageClass storageClass: longhorn
type: emptyDir accessMode: ReadWriteOnce
size: 10Gi
labels:
recurring-job.longhorn.io/source: enabled
recurring-job-group.longhorn.io/no-backup: enabled
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
+1
View File
@@ -3,3 +3,4 @@ kind: Kustomization
resources: resources:
- operator-crds-helm.yaml - operator-crds-helm.yaml
- operator-helm.yaml - operator-helm.yaml
- networkpolicy.yaml
+54
View File
@@ -0,0 +1,54 @@
# The mariadb-operator's three pods reach each other, DNS and the API
# server (the node, port 16443); the operator also connects to MariaDB on
# 3306 for the databases, users and grants it manages (mbgwp's
# mariadb-from-operator lets it in). Besides their own namespace they take
# connections only from the node: the API server calls the webhook from
# there and the kubelet probes come from there too (10.1.7.64 is the node's
# Calico VXLAN address, projectcalico.org/IPv4VXLANTunnelAddr). This
# folder's Application deploys into argocd (the operator's own
# Applications), hence the namespace here.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default
namespace: mariadb-system
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- from:
- ipBlock:
cidr: 192.168.1.204/32
- ipBlock:
cidr: 10.1.7.64/32
egress:
- to:
- podSelector: {}
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 192.168.1.204/32
ports:
- port: 16443
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
app.kubernetes.io/name: mariadb
ports:
- port: 3306
+35
View File
@@ -0,0 +1,35 @@
# Garage on the NAS
S3 storage for Velero, on the Synology (192.168.1.99) in Docker, outside
the cluster. Replaced MinIO, which is no longer maintained. One node, data
in `/volume1/docker/garage/{meta,data}`. S3 API on port 3900, admin API on
3903, both LAN only. Set up 2026-10-25 with Garage v2.5.0.
The NAS is reached as `ssh -p 34000 rubenhensen@192.168.1.99`; docker is
in `/usr/local/bin`, which a non-interactive SSH session doesn't have on
its PATH.
## Setup
`garage.toml` here has placeholders for the two secrets; on the NAS they
were filled in once with `openssl rand -hex 32` (`rpc_secret`) and
`openssl rand -base64 32` (`admin_token`), and only exist there.
```
cd /volume1/docker/garage
docker compose up -d
g="docker exec garage /garage"
$g layout assign -z nas -c 800G <node id from $g status>
$g layout apply --version 1
$g bucket create velero-backups
$g key create velero
$g bucket allow --read --write --key velero velero-backups
```
Velero's key is in Vault as `kv/velero-s3` (`access_key_id`,
`secret_access_key`) and reaches Velero through
`velero/external-secret.yaml`. Without the cluster, `$g key info velero
--show-secret` on the NAS shows it again.
Renovate updates the image tag in `compose.yaml`, but nothing deploys it:
after merging, `docker compose pull && docker compose up -d` on the NAS.
+12
View File
@@ -0,0 +1,12 @@
services:
garage:
image: dxflrs/garage:v2.5.0
container_name: garage
restart: unless-stopped
ports:
- "3900:3900" # S3 API
- "3903:3903" # admin API
volumes:
- ./garage.toml:/etc/garage.toml:ro
- ./meta:/var/lib/garage/meta
- ./data:/var/lib/garage/data
+29
View File
@@ -0,0 +1,29 @@
# One node on the Synology. Velero's kopia data is encrypted, so
# compression would only cost CPU.
#
# SQLite rather than the default LMDB: LMDB can be corrupted by an unclean
# shutdown, and a single node has no other node to repair from. With
# metadata_fsync SQLite runs WAL with synchronous=NORMAL, which only syncs
# at checkpoints; LMDB with metadata_fsync synced every transaction, ~270
# writes/s on the NAS's two disks with Garage idle. Converted 2026-10-25
# with `garage convert-db`.
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
metadata_fsync = true
data_fsync = true
metadata_auto_snapshot_interval = "6h"
replication_factor = 1
compression_level = "none"
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
rpc_secret = "@RPC_SECRET@"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
[admin]
api_bind_addr = "[::]:3903"
admin_token = "@ADMIN_TOKEN@"
+20 -3
View File
@@ -173,12 +173,18 @@ in
# - mail.rubenhensen.nl (and anything else) → local stalwart on 8443 # - mail.rubenhensen.nl (and anything else) → local stalwart on 8443
# - tunneled hosts → home cluster ingress on 443 # - tunneled hosts → home cluster ingress on 443
# - s1 / ntfy1.rubenhensen.nl → local nginx on 8444 (Gatus, ntfy) # - s1 / ntfy1.rubenhensen.nl → local nginx on 8444 (Gatus, ntfy)
#
# Each connection starts with a PROXY header that carries the visitor's
# address, so the home cluster sees who is asking instead of this box
# (gateway/proxy-protocol.yaml in k8scd). nginx sends the header to
# every upstream or none, so the local ones go through a hop on
# 8445/8446 that takes it off again.
streamConfig = '' streamConfig = ''
map $ssl_preread_server_name $tunnel_upstream { map $ssl_preread_server_name $tunnel_upstream {
${sniMapEntries} ${sniMapEntries}
s1.rubenhensen.nl 127.0.0.1:8444; s1.rubenhensen.nl 127.0.0.1:8446;
ntfy1.rubenhensen.nl 127.0.0.1:8444; ntfy1.rubenhensen.nl 127.0.0.1:8446;
default 127.0.0.1:8443; default 127.0.0.1:8445;
} }
server { server {
@@ -186,6 +192,17 @@ ${sniMapEntries}
listen [::]:443; listen [::]:443;
proxy_pass $tunnel_upstream; proxy_pass $tunnel_upstream;
ssl_preread on; ssl_preread on;
proxy_protocol on;
}
server {
listen 127.0.0.1:8445 proxy_protocol;
proxy_pass 127.0.0.1:8443;
}
server {
listen 127.0.0.1:8446 proxy_protocol;
proxy_pass 127.0.0.1:8444;
} }
''; '';
+1 -1
View File
@@ -16,7 +16,7 @@ spec:
spec: spec:
containers: containers:
- name: ntfy - name: ntfy
image: binwiederhier/ntfy:v2.28.0 image: binwiederhier/ntfy:v2.34.0
args: ["serve"] args: ["serve"]
env: env:
- name: NTFY_BASE_URL - name: NTFY_BASE_URL
+18 -2
View File
@@ -5,8 +5,16 @@
"customManagers:githubActionsVersions" "customManagers:githubActionsVersions"
], ],
"argocd": { "argocd": {
"description": "Every app folder; Renovate's regexes are RE2, which has no lookahead, so the other folders are skipped through ignorePaths",
"managerFilePatterns": [ "managerFilePatterns": [
"/^(?!(nix-infra-machine|backup-testing|docs|tools|\\.github)/)[^/]+/.+\\.yaml$/" "/^[^/]+/.+\\.yaml$/"
],
"ignorePaths": [
"nix-infra-machine/**",
"backup-testing/**",
"docs/**",
"tools/**",
".github/**"
] ]
}, },
"helm-values": { "helm-values": {
@@ -15,8 +23,16 @@
] ]
}, },
"kubernetes": { "kubernetes": {
"description": "Every app folder, as for argocd",
"managerFilePatterns": [ "managerFilePatterns": [
"/^(?!(nix-infra-machine|backup-testing|docs|tools|\\.github)/)[^/]+/.+\\.yaml$/" "/^[^/]+/.+\\.yaml$/"
],
"ignorePaths": [
"nix-infra-machine/**",
"backup-testing/**",
"docs/**",
"tools/**",
".github/**"
] ]
}, },
"customManagers": [ "customManagers": [
+1 -1
View File
@@ -3,7 +3,7 @@ module dqlite-vacuum
go 1.27.3 go 1.27.3
require ( require (
github.com/canonical/go-dqlite/v3 v3.0.1 github.com/canonical/go-dqlite/v3 v3.0.4
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
+5
View File
@@ -2,7 +2,10 @@ github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc=
github.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0= github.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0=
github.com/canonical/go-dqlite/v3 v3.0.1 h1:eUTcEHetVi1+zr+ZFz7PU0G+0eK66GZ7DKhi+wHs1f0= github.com/canonical/go-dqlite/v3 v3.0.1 h1:eUTcEHetVi1+zr+ZFz7PU0G+0eK66GZ7DKhi+wHs1f0=
github.com/canonical/go-dqlite/v3 v3.0.1/go.mod h1:Kb/9JVog9XeIlnlnSyO1JgMoE4PoKXZCB8fJNcvvHAc= github.com/canonical/go-dqlite/v3 v3.0.1/go.mod h1:Kb/9JVog9XeIlnlnSyO1JgMoE4PoKXZCB8fJNcvvHAc=
github.com/canonical/go-dqlite/v3 v3.0.4 h1:JhITsrvVL0pii9arxDksXfsRqBI+je1VpcqVuzDllUM=
github.com/canonical/go-dqlite/v3 v3.0.4/go.mod h1:6O+E6MiYesijRUOECyywp16iT+N4QOrvjDWWiMJ/xF0=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -26,7 +29,9 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+30
View File
@@ -0,0 +1,30 @@
# Velero's key for its bucket in Garage on the NAS (nas/garage), in the
# AWS credentials file format the aws plugin and kopia read.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: velero-s3-credentials
spec:
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
refreshInterval: 15m
target:
name: velero-s3-credentials
creationPolicy: Owner
template:
engineVersion: v2
data:
cloud: |
[default]
aws_access_key_id={{ .access_key_id }}
aws_secret_access_key={{ .secret_access_key }}
data:
- secretKey: access_key_id
remoteRef:
key: kv/velero-s3
property: access_key_id
- secretKey: secret_access_key
remoteRef:
key: kv/velero-s3
property: secret_access_key
-10
View File
@@ -1,10 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: nas-minio-external
spec:
type: ExternalName
externalName: 192.168.1.99
ports:
- port: 9000
protocol: TCP
+52
View File
@@ -0,0 +1,52 @@
# Velero, its node agent, and the pods it starts here (the data movers of
# each pod volume backup or restore, kopia's maintenance jobs, the chart's
# CRD upgrade job) reach each other, DNS, the API server (the node, port
# 16443) and Garage on the NAS (192.168.1.99:3900, nas/garage). The node
# agent reads volumes through its hostPath mount, not the network. Besides
# their own namespace they take connections only from the node (kubelet
# probes; 10.1.7.64 is the node's Calico VXLAN address,
# projectcalico.org/IPv4VXLANTunnelAddr); nothing scrapes the metrics on
# 8085. MinIO (the NAS, port 9000) is not allowed: going back to it means
# adding its port here as well.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- from:
- ipBlock:
cidr: 192.168.1.204/32
- ipBlock:
cidr: 10.1.7.64/32
egress:
- to:
- podSelector: {}
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 192.168.1.204/32
ports:
- port: 16443
- to:
- ipBlock:
cidr: 192.168.1.99/32
ports:
- port: 3900
+7
View File
@@ -1,6 +1,8 @@
# Volume policy for the backup schedules. emptyDir volumes hold nothing # Volume policy for the backup schedules. emptyDir volumes hold nothing
# worth keeping, and backing them up fails when the pod is gone before its # worth keeping, and backing them up fails when the pod is gone before its
# volume is read, as happens with Velero's own repository maintenance jobs. # volume is read, as happens with Velero's own repository maintenance jobs.
# PVCs in Longhorn's no-backup group (caches, such as immich's ML models)
# are skipped too.
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
@@ -14,3 +16,8 @@ data:
- emptyDir - emptyDir
action: action:
type: skip type: skip
- conditions:
pvcLabels:
recurring-job-group.longhorn.io/no-backup: enabled
action:
type: skip
+8 -9
View File
@@ -27,20 +27,22 @@ spec:
- mountPath: /target - mountPath: /target
name: plugins name: plugins
configuration: configuration:
# Garage on the NAS (nas/garage). Until 2026-10-26 this was MinIO on
# the same NAS; the bucket was copied over with rclone, so the
# existing backups and kopia repositories carried on.
backupStorageLocation: backupStorageLocation:
- name: default - name: default
bucket: velero-backups bucket: velero-backups
provider: aws provider: aws
config: config:
region: minio region: garage
s3ForcePathStyle: true s3ForcePathStyle: true
s3Url: http://192.168.1.99:9000 s3Url: http://192.168.1.99:3900
insecureSkipTLSVerify: "true"
volumeSnapshotLocation: volumeSnapshotLocation:
- name: default - name: default
provider: aws provider: aws
config: config:
region: minio region: garage
features: EnableCSI features: EnableCSI
# Kopia maintenance ran every hour for each of the 15 repositories, # Kopia maintenance ran every hour for each of the 15 repositories,
# and once a day as a full run of several minutes that writes its # and once a day as a full run of several minutes that writes its
@@ -59,11 +61,8 @@ spec:
memoryRequest: 256Mi memoryRequest: 256Mi
memoryLimit: 2Gi memoryLimit: 2Gi
credentials: credentials:
# Use existing secret created by External Secrets Operator # From Vault, velero/external-secret.yaml
# existingSecret: velero-minio-credentials existingSecret: velero-s3-credentials
# extraEnvVars:
# AWS_SHARED_CREDENTIALS_FILE: /credentials/cloud
existingSecret: cloud-credentials
deployNodeAgent: true deployNodeAgent: true
nodeAgent: nodeAgent:
containerSecurityContext: containerSecurityContext: