Compare commits

...

6 Commits

Author SHA1 Message Date
henry 2da631dc81 UPDATE: modify user and group IDs in Immich configuration; adjust NFS paths for persistent volumes 2026-04-26 12:38:21 +02:00
henry 76945105d7 ADD: implement Gitea persistent volume and claim; migrate PostgreSQL from NFS to Longhorn with updated configurations
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 20:34:59 +02:00
henry 9905abd9b4 ADD: implement migration scripts for PostgreSQL to Longhorn; update Immich configurations for namespace and persistent storage
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 18:51:42 +02:00
henry 1125b8b072 ADD: update Nextcloud and Gitea configurations for domain and Docker settings; enhance Homarr deployment with resource limits and OIDC authentication
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 14:13:07 +02:00
henry 39079615f5 Add migration scripts and manifests for GitLab and Gitea to Longhorn
- Create .vscode/settings.json for YAML schema validation.
- Add WISSENSBASIS.md for documentation on HomeLabScripts.
- Implement migration job for GitLab from NFS to Longhorn with migrate-to-longhorn.yaml and migrate-to-longhorn.sh.
- Add Gitea migration scripts and manifests for PostgreSQL to Longhorn.
- Create persistent volume claims and deployments for Gitea and Homarr.
- Set up namespaces and services for Homarr and Speedtest Tracker.
- Add secrets for Homarr and Speedtest Tracker with sensitive data.
- Configure Ingress for Speedtest Tracker with Traefik annotations.

Co-authored-by: Copilot <copilot@github.com>
2026-04-24 23:08:23 +02:00
henry be9329d313 ADD: implement migration and backup configurations for Nextcloud with Longhorn support 2026-04-15 19:51:29 +02:00
54 changed files with 1793 additions and 118 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"yaml.schemas": {
"kubernetes://schema/v1@persistentvolumeclaim": "file:///home/henry/HomeLabScripts/k3s/apps/gitLab/manifest/pv-pvc.yaml"
}
}
+97
View File
@@ -0,0 +1,97 @@
# HomeLabScripts Wissensbasis
## Zweck
Persönliches Home-Lab-Infrastruktur-Repository. Orchestriert eine selbst gehostete Cloud-Umgebung auf K3s (Kubernetes) auf lokaler Hardware.
## Technologien
- **K3s** Kubernetes-Distribution
- **Helm** Paketmanager für K8s
- **NFS** Netzwerk-Dateisystem (shared storage)
- **Longhorn** verteilter Block-Storage (2 Replicas)
- **PostgreSQL / MariaDB / Redis** Datenbanken & Caching
- **Authentik** OAuth2/OIDC-Authentifizierungsserver
---
## Repo-Struktur
```
HomeLabScripts/
├── k3s/
│ ├── apps/ # App-Manifeste & Helm-Charts
│ ├── install.sh # K3s-Cluster-Installation
│ ├── get_helm.sh # Helm herunterladen
│ ├── installHelm.sh # Helm installieren
│ └── k8sUser/ # Benutzer-/Kubeconfig-Setup
├── nfs/ # NFS-Server- & Client-Skripte
└── mountscript/ # Disk-Partitionierung & Einhängen
```
---
## Anwendungen
| App | Namespace | NodePort | Storage | Beschreibung |
|-----|-----------|----------|---------|--------------|
| **Authentik** | authentik | 32222 | PostgreSQL (intern) | OAuth2/OIDC-Provider |
| **Homarr** | homarr | 30757 | Longhorn 5Gi | Homepage-Dashboard |
| **K8s Dashboard** | kubernetes-dashboard | 443 | | Cluster-Management-UI |
| **Gitea** | gitea | | NFS 30Gi (Repos) + 10Gi (DB) | Leichter Git-Dienst |
| **GitLab** | gitlab | 80/443/22 | NFS 50Gi (RWX) | Full GitLab mit CI/CD |
| **Nextcloud** | nextcloud | 30180 | NFS (Daten) + Longhorn (DB) | Datei-Hosting |
| **Immich** | photoprism | 3001/2283 | NFS photos | Fotoverwaltung (Google Photos Alternative) |
| **PhotoPrism** | photoprism | | NFS photos | KI-Fotoverwaltung |
| **iCloudPD** | photoprism | | NFS /data/originals | Apple-iCloud-Foto-Sync |
| **Longhorn** | longhorn-system | | | Storage-Provisioner |
---
## Storage-Strategie
- **Longhorn** → Datenbanken, kleine Konfigurationen (schnell, lokal)
- **NFS** → Medien, Repos, Nextcloud-Daten (groß, geteilt, RWX)
- NFS-Server: `192.168.178.166`, Pfade: `/export/fastData/`, `/export/slowData/`
---
## Netzwerk
- **NodePort** für externen Zugriff
- **ClusterIP** für Pod-to-Pod-Kommunikation (DBs)
- **Multus** bei Immich (separates IoT-Netz: `192.168.1.192/24`)
- Domain: `henryathome.home64.de`
---
## Datenbank-Zuordnung
| App | DB-Typ | User | Hinweis |
|-----|--------|------|---------|
| Authentik | PostgreSQL | | intern |
| Nextcloud | MariaDB 10.8 | nextcloud | PW: nextcloud |
| GitLab | MariaDB | | NFS-Backend |
| Gitea | PostgreSQL | | NFS-Backend |
| Immich | PostgreSQL 14 (pgvecto-rs) | immich | PW: password |
| PhotoPrism | MariaDB | photoprism | PW: photoprism |
---
## Wichtige Skripte
| Skript | Zweck |
|--------|-------|
| `k3s/install.sh` | K3s installieren |
| `k3s/installHelm.sh` | Helm 3 installieren |
| `k3s/k8sUser/addUser.sh` | ServiceAccount + ClusterRoleBinding + Kubeconfig erstellen |
| `k3s/apps/dashboard/getToken.sh` | Admin-Token für K8s Dashboard |
| `k3s/apps/photo/icloudpd/base64pw.sh` | iCloud-Passwort base64-kodieren |
| `k3s/apps/Nextcloud/helm/cleanRestart.sh` | Nextcloud sauber neu starten |
| `nfs/server.sh` | NFS-Server konfigurieren |
| `nfs/client.sh` / `nfsClient2.sh` | NFS-Client einrichten & in fstab eintragen |
| `mountscript/mount-plus.sh` | Festplatte partitionieren, formatieren, einhängen |
---
## Muster & Konventionen
- Secrets: `*-secret.yaml` je App, base64-kodiert
- Init-Container: Warten auf DB-Bereitschaft (Nextcloud, Immich)
- `imagePullPolicy: IfNotPresent` (kein automatisches Re-Pull)
- `nodeSelector: knode0` bei Nextcloud (Kernel 6.1 für NFS)
- fsGroup für NFS-Berechtigungen (z. B. `33` für www-data)
- GitLab-Runner-Token: `glrt-3nNma_nEvL1Bq2zc8m5Zu286MQpwOjIKdDozCnU6MTAQ`
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Pod
metadata:
name: mariadb-backup
namespace: nextcloud
spec:
nodeName: knode0 # <-- FIXED ON node0
containers:
- name: backup
image: busybox
command: ["sleep", "3600"]
volumeMounts:
- name: old
mountPath: /old
volumes:
- name: old
persistentVolumeClaim:
claimName: nextcloud-mariadb-pvc
@@ -0,0 +1,34 @@
apiVersion: v1
kind: Pod
metadata:
name: config-migration
namespace: nextcloud
spec:
volumes:
- name: old-config
persistentVolumeClaim:
claimName: nextcloud-config-pvc
- name: new-config
persistentVolumeClaim:
claimName: nextcloud-config-pvc-longhorn
containers:
- name: migrator
image: alpine:3.18
volumeMounts:
- name: old-config
mountPath: /old
- name: new-config
mountPath: /new
command: ['sh']
args:
- -c
- |
echo "Copying config data..."
if [ "$(ls -A /old)" ]; then
cp -rv /old/* /new/ 2>/dev/null || true
echo "Config migration completed"
else
echo "Old config is empty"
fi
sleep infinity
restartPolicy: Never
@@ -0,0 +1,34 @@
apiVersion: v1
kind: Pod
metadata:
name: apps-migration
namespace: nextcloud
spec:
volumes:
- name: old-apps
persistentVolumeClaim:
claimName: nextcloud-apps-pvc
- name: new-apps
persistentVolumeClaim:
claimName: nextcloud-apps-pvc-longhorn
containers:
- name: migrator
image: alpine:3.18
volumeMounts:
- name: old-apps
mountPath: /old
- name: new-apps
mountPath: /new
command: ['sh']
args:
- -c
- |
echo "Copying apps data..."
if [ "$(ls -A /old)" ]; then
cp -rv /old/* /new/ 2>/dev/null || true
echo "Apps migration completed"
else
echo "Old apps is empty"
fi
sleep infinity
restartPolicy: Never
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-mariadb-pvc-longhorn
namespace: nextcloud
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 10Gi
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Pod
metadata:
name: mariadb-restore
namespace: nextcloud
spec:
nodeName: knode0
containers:
- name: restore
image: busybox
command: ["sleep", "3600"]
volumeMounts:
- name: new
mountPath: /new
- name: oldbackup
mountPath: /backup
volumes:
- name: new
persistentVolumeClaim:
claimName: nextcloud-mariadb-pvc-longhorn
- name: oldbackup
hostPath:
path: /var/lib/nextcloud/mariadb-backup
type: DirectoryOrCreate
@@ -28,6 +28,11 @@ spec:
value: nextcloud value: nextcloud
ports: ports:
- containerPort: 3306 - containerPort: 3306
readinessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 5
periodSeconds: 10
resources: resources:
requests: requests:
memory: "256Mi" memory: "256Mi"
@@ -41,4 +46,4 @@ spec:
volumes: volumes:
- name: mariadb-data - name: mariadb-data
persistentVolumeClaim: persistentVolumeClaim:
claimName: nextcloud-mariadb-pvc claimName: nextcloud-mariadb-pvc-longhorn
@@ -9,24 +9,4 @@ spec:
resources: resources:
requests: requests:
storage: 10Gi storage: 10Gi
storageClassName: local-path # <-- explicit default StorageClass storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: nextcloud-mariadb-pv
labels:
app: mariadb
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-path
claimRef:
namespace: nextcloud
name: nextcloud-mariadb-pvc
hostPath:
path: /var/lib/nextcloud/mariadb-data
type: DirectoryOrCreate
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-apps-pvc-longhorn
namespace: nextcloud
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-config-pvc-longhorn
namespace: nextcloud
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 5Gi
@@ -16,12 +16,19 @@ spec:
# fsGroup sorgt dafür, dass gemountete Volumes die Gruppe www-data (33) bekommen # fsGroup sorgt dafür, dass gemountete Volumes die Gruppe www-data (33) bekommen
securityContext: securityContext:
fsGroup: 33 fsGroup: 33
# Auf knode0 zwingen (hat Kernel 6.1 mit NFS - Kompatibilität)
nodeSelector:
kubernetes.io/hostname: knode0
# hostAliases mappt die öffentliche Domain intern auf die Service-ClusterIP, # hostAliases mappt die öffentliche Domain intern auf die Service-ClusterIP,
# damit der Pod henryathome.home64.de direkt intern erreicht (vermeidet externe Loopback/Firewall/403) # damit der Pod henryathome.home64.de direkt intern erreicht (vermeidet externe Loopback/Firewall/403)
hostAliases: hostAliases:
- ip: "10.43.107.87" - ip: "10.43.107.87"
hostnames: hostnames:
- "henryathome.home64.de" - "henryathome.home64.de"
initContainers:
- name: wait-for-mariadb
image: busybox:1.34
command: ['sh', '-c', 'until nc -z mariadb.nextcloud.svc.cluster.local 3306; do echo waiting for mariadb; sleep 2; done;']
containers: containers:
- name: nextcloud - name: nextcloud
image: nextcloud:33-apache image: nextcloud:33-apache
@@ -44,15 +51,15 @@ spec:
- name: REDIS_HOST - name: REDIS_HOST
value: redis.nextcloud.svc.cluster.local value: redis.nextcloud.svc.cluster.local
- name: NEXTCLOUD_TRUSTED_DOMAINS - name: NEXTCLOUD_TRUSTED_DOMAINS
value: "henryathome.home64.de,192.168.178.0/24,192.168.178.138,nextcloud.nextcloud.svc.cluster.local" value: "cloud.henryathome.home64.de,192.168.178.0/24,192.168.178.138,nextcloud.nextcloud.svc.cluster.local"
- name: TRUSTED_PROXIES - name: TRUSTED_PROXIES
value: "192.168.178.120" value: "192.168.178.120"
- name: OVERWRITEHOST - name: OVERWRITEHOST
value: "henryathome.home64.de" value: "cloud.henryathome.home64.de"
- name: OVERWRITEPROTOCOL - name: OVERWRITEPROTOCOL
value: "https" value: "https"
- name: OVERWRITECLIURL - name: OVERWRITECLIURL
value: "https://henryathome.home64.de" value: "https://cloud.henryathome.home64.de"
resources: resources:
requests: requests:
memory: "512Mi" memory: "512Mi"
@@ -73,7 +80,7 @@ spec:
claimName: nextcloud-data-pvc claimName: nextcloud-data-pvc
- name: config - name: config
persistentVolumeClaim: persistentVolumeClaim:
claimName: nextcloud-config-pvc claimName: nextcloud-config-pvc-longhorn
- name: apps - name: apps
persistentVolumeClaim: persistentVolumeClaim:
claimName: nextcloud-apps-pvc claimName: nextcloud-apps-pvc-longhorn
@@ -10,10 +10,13 @@ spec:
persistentVolumeReclaimPolicy: Retain persistentVolumeReclaimPolicy: Retain
storageClassName: nfs storageClassName: nfs
mountOptions: mountOptions:
- vers=4 - vers=3
- rsize=65536 - rsize=65536
- wsize=65536 - wsize=65536
- noatime - noatime
- soft
- timeo=20
- retrans=2
nfs: nfs:
server: 192.168.178.186 # <-- ERSETZEN: IP oder Hostname deiner NAS server: 192.168.178.186 # <-- ERSETZEN: IP oder Hostname deiner NAS
path: /volume1/Nextcloud/data # <-- ERSETZEN: Pfad zum Share auf der NAS (z.B. /volume1/nextcloud) path: /volume1/Nextcloud/data # <-- ERSETZEN: Pfad zum Share auf der NAS (z.B. /volume1/nextcloud)
+495
View File
@@ -0,0 +1,495 @@
#!/usr/bin/env bash
set -euo pipefail
NAMESPACE="authentik"
RELEASE="authentik"
CHART="authentik/authentik"
VALUES_FILE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/values.yaml"
TARGET_PVC="authentik-postgresql-longhorn-pvc"
LONGHORN_STORAGE_CLASS="longhorn"
JOB_NAME="authentik-postgres-migrate-to-longhorn"
PVC_BIND_TIMEOUT_SECONDS=900
POD_STOP_TIMEOUT_SECONDS=300
JOB_TIMEOUT_SECONDS=3600
POSTGRES_READY_TIMEOUT_SECONDS=300
DRY_RUN="false"
AUTO_CONFIRM="false"
ORIGINAL_POSTGRES_REPLICAS="1"
SOURCE_NODE_NAME=""
SOURCE_PVC=""
SOURCE_STORAGE=""
POSTGRES_STS=""
POSTGRES_POD=""
DEPLOYMENTS_TO_RESTORE=()
DEPLOYMENT_REPLICAS=()
AUTHENTIK_SCALED_DOWN="false"
POSTGRES_SCALED_DOWN="false"
info() {
echo "[INFO] $*"
}
warn() {
echo "[WARN] $*" >&2
}
err() {
echo "[ERROR] $*" >&2
exit 1
}
usage() {
cat <<'EOF'
Usage: migrate-postgres-to-longhorn.sh [options]
Options:
--namespace <name> Kubernetes namespace (default: authentik)
--release <name> Helm release name (default: authentik)
--chart <ref> Helm chart ref/path (default: authentik/authentik)
--values <path> Helm values file to use (default: ./values.yaml)
--target-pvc <name> Target Longhorn PVC name (default: authentik-postgresql-longhorn-pvc)
--storage-class <name> StorageClass for target PVC (default: longhorn)
--dry-run Print plan without changing cluster state
--yes Skip interactive confirmation
--help Show this help
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--namespace)
NAMESPACE="$2"
shift
;;
--release)
RELEASE="$2"
shift
;;
--chart)
CHART="$2"
shift
;;
--values)
VALUES_FILE="$2"
shift
;;
--target-pvc)
TARGET_PVC="$2"
shift
;;
--storage-class)
LONGHORN_STORAGE_CLASS="$2"
shift
;;
--dry-run)
DRY_RUN="true"
;;
--yes)
AUTO_CONFIRM="true"
;;
--help|-h)
usage
exit 0
;;
*)
err "Unknown argument: $1"
;;
esac
shift
done
}
require_tool() {
local tool="$1"
command -v "${tool}" >/dev/null 2>&1 || err "Required tool not found: ${tool}"
}
require_file() {
local path="$1"
[[ -f "${path}" ]] || err "Required file not found: ${path}"
}
wait_for_pvc_bound() {
local pvc_name="$1"
local timeout="$2"
local elapsed=0
local interval=5
info "Waiting for PVC ${pvc_name} to become Bound..."
while true; do
local phase
phase="$(kubectl -n "${NAMESPACE}" get pvc "${pvc_name}" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
if [[ "${phase}" == "Bound" ]]; then
info "PVC ${pvc_name} is Bound."
return 0
fi
if (( elapsed >= timeout )); then
err "Timeout while waiting for PVC ${pvc_name} to bind."
fi
sleep "${interval}"
elapsed=$((elapsed + interval))
done
}
wait_for_statefulset_replicas() {
local sts_name="$1"
local expected="$2"
local timeout="$3"
local elapsed=0
local interval=5
info "Waiting for StatefulSet ${sts_name} to reach ${expected} ready replicas..."
while true; do
local ready
ready="$(kubectl -n "${NAMESPACE}" get sts "${sts_name}" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)"
ready="${ready:-0}"
if [[ "${ready}" == "${expected}" ]]; then
info "StatefulSet ${sts_name} has ${expected} ready replicas."
return 0
fi
if (( elapsed >= timeout )); then
err "Timeout while waiting for StatefulSet ${sts_name} ready replicas ${expected}."
fi
sleep "${interval}"
elapsed=$((elapsed + interval))
done
}
wait_for_no_pods_for_release() {
local selector="$1"
local timeout="$2"
local elapsed=0
local interval=5
info "Waiting for pods with selector ${selector} to stop..."
while true; do
local count
count="$(kubectl -n "${NAMESPACE}" get pods -l "${selector}" --no-headers 2>/dev/null | awk '$3 != "Completed" && $3 != "Succeeded" {count++} END {print count+0}')"
if [[ "${count}" == "0" ]]; then
info "No active pods left for selector ${selector}."
return 0
fi
if (( elapsed >= timeout )); then
err "Timeout while waiting for pods with selector ${selector} to stop."
fi
sleep "${interval}"
elapsed=$((elapsed + interval))
done
}
discover_postgres_runtime() {
POSTGRES_STS="$(kubectl -n "${NAMESPACE}" get sts -l "app.kubernetes.io/name=postgresql,app.kubernetes.io/instance=${RELEASE}" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
[[ -n "${POSTGRES_STS}" ]] || err "Could not find PostgreSQL StatefulSet for release ${RELEASE}."
POSTGRES_POD="${POSTGRES_STS}-0"
SOURCE_PVC="$(kubectl -n "${NAMESPACE}" get sts "${POSTGRES_STS}" -o jsonpath='{.spec.volumeClaimTemplates[0].metadata.name}' 2>/dev/null || true)"
[[ -n "${SOURCE_PVC}" ]] || err "Could not detect PostgreSQL volumeClaimTemplate name from StatefulSet ${POSTGRES_STS}."
SOURCE_PVC="${SOURCE_PVC}-${POSTGRES_STS}-0"
kubectl -n "${NAMESPACE}" get pvc "${SOURCE_PVC}" >/dev/null 2>&1 || err "Source PVC not found: ${SOURCE_PVC}"
SOURCE_NODE_NAME="$(kubectl -n "${NAMESPACE}" get pod "${POSTGRES_POD}" -o jsonpath='{.spec.nodeName}' 2>/dev/null || true)"
SOURCE_STORAGE="$(kubectl -n "${NAMESPACE}" get pvc "${SOURCE_PVC}" -o jsonpath='{.spec.resources.requests.storage}' 2>/dev/null || true)"
[[ -n "${SOURCE_STORAGE}" ]] || err "Could not read requested storage size from source PVC ${SOURCE_PVC}."
ORIGINAL_POSTGRES_REPLICAS="$(kubectl -n "${NAMESPACE}" get sts "${POSTGRES_STS}" -o jsonpath='{.spec.replicas}' 2>/dev/null || true)"
ORIGINAL_POSTGRES_REPLICAS="${ORIGINAL_POSTGRES_REPLICAS:-1}"
info "Detected PostgreSQL StatefulSet: ${POSTGRES_STS}"
info "Detected source PVC: ${SOURCE_PVC}"
if [[ -n "${SOURCE_NODE_NAME}" ]]; then
info "Detected source node: ${SOURCE_NODE_NAME}"
else
info "Source PostgreSQL pod currently not running, proceeding without node pinning"
fi
info "Detected source requested storage: ${SOURCE_STORAGE}"
}
ensure_target_pvc() {
info "Ensuring Longhorn target PVC ${TARGET_PVC} exists."
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${TARGET_PVC}
namespace: ${NAMESPACE}
spec:
storageClassName: ${LONGHORN_STORAGE_CLASS}
accessModes:
- ReadWriteOnce
resources:
requests:
storage: ${SOURCE_STORAGE}
EOF
wait_for_pvc_bound "${TARGET_PVC}" "${PVC_BIND_TIMEOUT_SECONDS}"
}
scale_down_authentik() {
local deployment_lines
deployment_lines="$(kubectl -n "${NAMESPACE}" get deploy -l "app.kubernetes.io/instance=${RELEASE}" -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.replicas}{"\n"}{end}' 2>/dev/null || true)"
if [[ -z "${deployment_lines}" ]]; then
warn "No Authentik deployments found for release ${RELEASE}."
return 0
fi
while IFS=$'\t' read -r name replicas; do
[[ -n "${name}" ]] || continue
replicas="${replicas:-1}"
DEPLOYMENTS_TO_RESTORE+=("${name}")
DEPLOYMENT_REPLICAS+=("${replicas}")
if [[ "${replicas}" != "0" ]]; then
info "Scaling deployment ${name} to 0."
kubectl -n "${NAMESPACE}" scale deploy "${name}" --replicas=0 >/dev/null
fi
done <<< "${deployment_lines}"
AUTHENTIK_SCALED_DOWN="true"
wait_for_no_pods_for_release "app.kubernetes.io/instance=${RELEASE},app.kubernetes.io/name=authentik" "${POD_STOP_TIMEOUT_SECONDS}"
}
scale_down_postgres() {
info "Scaling StatefulSet ${POSTGRES_STS} to 0 for consistent copy."
kubectl -n "${NAMESPACE}" scale sts "${POSTGRES_STS}" --replicas=0 >/dev/null
POSTGRES_SCALED_DOWN="true"
wait_for_statefulset_replicas "${POSTGRES_STS}" "0" "${POD_STOP_TIMEOUT_SECONDS}"
}
run_migration_job() {
info "Recreating migration job ${JOB_NAME}."
kubectl -n "${NAMESPACE}" delete job "${JOB_NAME}" --ignore-not-found >/dev/null
cat <<EOF | kubectl apply -f - >/dev/null
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB_NAME}
namespace: ${NAMESPACE}
spec:
backoffLimit: 2
template:
metadata:
name: ${JOB_NAME}
spec:
restartPolicy: Never
containers:
- name: migrate
image: alpine:3.20
command:
- sh
- -c
- |
set -euo pipefail
apk add --no-cache rsync findutils coreutils
src_count="\$(find /source -mindepth 1 | wc -l | tr -d ' ')"
echo "Source entries before copy: \${src_count}"
rsync -aHAX --numeric-ids --delete /source/ /target/
target_count="\$(find /target -mindepth 1 | wc -l | tr -d ' ')"
echo "Target entries after copy: \${target_count}"
if [ -f /target/PG_VERSION ] && [ -d /target/base ]; then
pg_root="/target"
elif [ -f /target/data/PG_VERSION ] && [ -d /target/data/base ]; then
pg_root="/target/data"
else
echo "Could not find PostgreSQL data root on target" >&2
echo "Top-level target contents:" >&2
ls -la /target >&2 || true
echo "Nested target/data contents:" >&2
ls -la /target/data >&2 || true
exit 1
fi
test "\${target_count}" -gt 0
echo "Detected PostgreSQL data root: \${pg_root}"
echo "Top-level target contents:"
ls -la /target
echo "Migration verification successful"
volumeMounts:
- name: source
mountPath: /source
readOnly: true
- name: target
mountPath: /target
volumes:
- name: source
persistentVolumeClaim:
claimName: ${SOURCE_PVC}
- name: target
persistentVolumeClaim:
claimName: ${TARGET_PVC}
EOF
info "Streaming migration job logs."
kubectl -n "${NAMESPACE}" logs -f "job/${JOB_NAME}" || true
info "Waiting for migration job completion."
if ! kubectl -n "${NAMESPACE}" wait --for=condition=complete "job/${JOB_NAME}" --timeout="${JOB_TIMEOUT_SECONDS}s" >/dev/null; then
kubectl -n "${NAMESPACE}" describe job "${JOB_NAME}" || true
kubectl -n "${NAMESPACE}" logs "job/${JOB_NAME}" --tail=-1 || true
err "Migration job failed."
fi
info "Migration job completed."
}
verify_target_pvc_has_data() {
info "Verifying copied PostgreSQL markers on target PVC ${TARGET_PVC}."
kubectl -n "${NAMESPACE}" run authentik-postgres-verify \
--rm --restart=Never --image=alpine:3.20 \
--overrides="{\"apiVersion\":\"v1\",\"spec\":{\"containers\":[{\"name\":\"verify\",\"image\":\"alpine:3.20\",\"command\":[\"sh\",\"-c\",\"set -e; if [ -f /target/PG_VERSION ] && [ -d /target/base ]; then root=/target; elif [ -f /target/data/PG_VERSION ] && [ -d /target/data/base ]; then root=/target/data; else echo missing-postgres-layout >&2; ls -la /target >&2 || true; ls -la /target/data >&2 || true; exit 1; fi; echo detected-root=\$root; find /target -mindepth 1 | wc -l; ls -la /target | head -n 20\"],\"volumeMounts\":[{\"name\":\"target\",\"mountPath\":\"/target\"}]}],\"volumes\":[{\"name\":\"target\",\"persistentVolumeClaim\":{\"claimName\":\"${TARGET_PVC}\"}}]}}" \
--attach=true >/dev/null
}
helm_switch_postgres_to_target_pvc() {
info "Upgrading Helm release ${RELEASE} to use existing PostgreSQL claim ${TARGET_PVC}."
helm -n "${NAMESPACE}" upgrade --install "${RELEASE}" "${CHART}" \
-f "${VALUES_FILE}" \
--reuse-values \
--set "postgresql.primary.persistence.enabled=true" \
--set "postgresql.primary.persistence.existingClaim=${TARGET_PVC}" \
--set "authentik.existingSecret.secretName=" >/dev/null
}
verify_postgres_uses_target_claim() {
info "Waiting for PostgreSQL StatefulSet to become ready after Helm upgrade."
wait_for_statefulset_replicas "${POSTGRES_STS}" "1" "${POSTGRES_READY_TIMEOUT_SECONDS}"
local mounted_claim
mounted_claim="$(kubectl -n "${NAMESPACE}" get pod "${POSTGRES_STS}-0" -o jsonpath='{.spec.volumes[?(@.name=="data")].persistentVolumeClaim.claimName}' 2>/dev/null || true)"
if [[ -z "${mounted_claim}" ]]; then
mounted_claim="$(kubectl -n "${NAMESPACE}" get pod "${POSTGRES_STS}-0" -o jsonpath='{.spec.volumes[?(@.persistentVolumeClaim)].persistentVolumeClaim.claimName}' 2>/dev/null | awk '{print $1}')"
fi
[[ -n "${mounted_claim}" ]] || err "Could not detect mounted PostgreSQL claim after upgrade."
[[ "${mounted_claim}" == "${TARGET_PVC}" ]] || err "PostgreSQL pod mounts ${mounted_claim}, expected ${TARGET_PVC}."
info "PostgreSQL now mounts target claim ${mounted_claim}."
}
restore_authentik_deployments() {
if [[ "${AUTHENTIK_SCALED_DOWN}" != "true" ]]; then
return 0
fi
local index
for index in "${!DEPLOYMENTS_TO_RESTORE[@]}"; do
local name="${DEPLOYMENTS_TO_RESTORE[${index}]}"
local replicas="${DEPLOYMENT_REPLICAS[${index}]}"
if [[ "${replicas}" == "0" ]]; then
info "Skipping restore of deployment ${name} to 0 replicas."
continue
fi
info "Restoring deployment ${name} replicas to ${replicas}."
kubectl -n "${NAMESPACE}" scale deploy "${name}" --replicas="${replicas}" >/dev/null
done
AUTHENTIK_SCALED_DOWN="false"
}
restore_on_error() {
if [[ "${POSTGRES_SCALED_DOWN}" == "true" ]]; then
warn "Restoring StatefulSet ${POSTGRES_STS} replicas to ${ORIGINAL_POSTGRES_REPLICAS}."
kubectl -n "${NAMESPACE}" scale sts "${POSTGRES_STS}" --replicas="${ORIGINAL_POSTGRES_REPLICAS}" >/dev/null || true
POSTGRES_SCALED_DOWN="false"
fi
restore_authentik_deployments || true
}
on_exit() {
local exit_code=$?
if [[ ${exit_code} -ne 0 ]]; then
restore_on_error
warn "Script failed with exit code ${exit_code}."
fi
}
trap on_exit EXIT
validate_prerequisites() {
require_tool kubectl
require_tool helm
require_file "${VALUES_FILE}"
kubectl get namespace "${NAMESPACE}" >/dev/null 2>&1 || err "Namespace does not exist: ${NAMESPACE}"
helm -n "${NAMESPACE}" status "${RELEASE}" >/dev/null 2>&1 || err "Helm release not found: ${RELEASE} in namespace ${NAMESPACE}"
}
confirm_migration() {
if [[ "${AUTO_CONFIRM}" == "true" ]]; then
return 0
fi
echo
warn "Authentik and PostgreSQL workloads in namespace ${NAMESPACE} will be scaled down during migration."
read -r -p "Type MIGRATE to continue: " confirmation
if [[ "${confirmation}" != "MIGRATE" ]]; then
err "Confirmation failed. Aborted by user."
fi
}
print_dry_run_plan() {
info "Dry-run mode active. No cluster changes will be made."
info "Planned steps:"
info "1) Discover PostgreSQL StatefulSet, pod, source PVC, and size"
info "2) Create Longhorn target PVC ${TARGET_PVC}"
info "3) Scale down Authentik deployments and PostgreSQL StatefulSet"
info "4) Run rsync migration job ${JOB_NAME}"
info "5) Verify copied DB files on target PVC"
info "6) Helm upgrade release ${RELEASE} with postgresql.primary.persistence.existingClaim=${TARGET_PVC}"
info "7) Verify new PostgreSQL pod uses target PVC"
info "8) Restore Authentik deployments"
}
run_migration() {
discover_postgres_runtime
if [[ "${DRY_RUN}" == "true" ]]; then
print_dry_run_plan
return 0
fi
confirm_migration
ensure_target_pvc
scale_down_authentik
scale_down_postgres
run_migration_job
verify_target_pvc_has_data
helm_switch_postgres_to_target_pvc
POSTGRES_SCALED_DOWN="false"
verify_postgres_uses_target_claim
restore_authentik_deployments
info "Migration finished successfully."
info "PostgreSQL is now running on Longhorn PVC ${TARGET_PVC}."
}
main() {
parse_args "$@"
info "Starting Authentik PostgreSQL migration to Longhorn PVC."
validate_prerequisites
run_migration
}
main "$@"
+47 -1
View File
@@ -2,6 +2,8 @@
namespace: authentik namespace: authentik
authentik: authentik:
secret_key: "6sNotXqR3cvcVHx3RbYCViX6J/OmMvopb4b7ge80V3EdSgBtWzG0l4SXBPo80J3mRy0BDaCCfb1EZoz+" secret_key: "6sNotXqR3cvcVHx3RbYCViX6J/OmMvopb4b7ge80V3EdSgBtWzG0l4SXBPo80J3mRy0BDaCCfb1EZoz+"
existingSecret:
secretName: ""
# This sends anonymous usage-data, stack traces on errors and # This sends anonymous usage-data, stack traces on errors and
# performance data to sentry.io, and is fully opt-in # performance data to sentry.io, and is fully opt-in
error_reporting: error_reporting:
@@ -9,6 +11,16 @@ authentik:
postgresql: postgresql:
password: "WoPbKRCEeLoLb9J840FqwDE95ergX8CqXq7jC6nbJkoNSiTSlA" password: "WoPbKRCEeLoLb9J840FqwDE95ergX8CqXq7jC6nbJkoNSiTSlA"
global:
volumeMounts:
- name: authentik-rbac-migration-fix
mountPath: /authentik/rbac/migrations/0010_remove_role_group_alter_role_name.py
subPath: 0010_remove_role_group_alter_role_name.py
volumes:
- name: authentik-rbac-migration-fix
configMap:
name: authentik-rbac-migration-fix
server: server:
ingress: ingress:
# Specify kubernetes ingress controller class name # Specify kubernetes ingress controller class name
@@ -25,4 +37,38 @@ server:
postgresql: postgresql:
enabled: true enabled: true
auth: auth:
password: "WoPbKRCEeLoLb9J840FqwDE95ergX8CqXq7jC6nbJkoNSiTSlA" password: "WoPbKRCEeLoLb9J840FqwDE95ergX8CqXq7jC6nbJkoNSiTSlA"
primary:
persistence:
enabled: true
existingClaim: authentik-postgresql-longhorn-pvc
additionalObjects:
- apiVersion: v1
kind: ConfigMap
metadata:
name: authentik-rbac-migration-fix
namespace: authentik
data:
0010_remove_role_group_alter_role_name.py: |
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_rbac", "0009_remove_initialpermissions_mode"),
("authentik_core", "0056_user_roles"),
]
operations = [
migrations.RemoveField(
model_name="role",
name="group",
),
migrations.AlterField(
model_name="role",
name="name",
field=models.TextField(unique=True),
),
]
@@ -99,7 +99,7 @@ spec:
volumes: volumes:
- name: gitlab-data # lokal (postgresql, redis, etc.) - name: gitlab-data # lokal (postgresql, redis, etc.)
persistentVolumeClaim: persistentVolumeClaim:
claimName: gitlab-data-pvc claimName: gitlab-data-longhorn-pvc
- name: gitlab-git # NFS (Git-Repositories) - name: gitlab-git # NFS (Git-Repositories)
persistentVolumeClaim: persistentVolumeClaim:
@@ -107,7 +107,7 @@ spec:
- name: gitlab-config # lokal - name: gitlab-config # lokal
persistentVolumeClaim: persistentVolumeClaim:
claimName: gitlab-config-pvc claimName: gitlab-config-longhorn-pvc
- name: gitlab-logs # ephemeral - name: gitlab-logs # ephemeral
emptyDir: {} emptyDir: {}
@@ -0,0 +1,48 @@
apiVersion: batch/v1
kind: Job
metadata:
name: gitlab-migrate-to-longhorn
namespace: gitlab
spec:
backoffLimit: 4
template:
metadata:
name: gitlab-migrate-to-longhorn
spec:
restartPolicy: Never
containers:
- name: migrate
image: alpine:3.18
command:
- sh
- -c
- |
set -euo pipefail
apk add --no-cache rsync
echo "Starting migration: /var/opt/gitlab (data)"
rsync -aHAX --numeric-ids --delete /var/opt_gitlab_old/ /var/opt_gitlab_new/
echo "Finished data copy. Starting config copy: /etc/gitlab"
rsync -aHAX --numeric-ids --delete /etc_gitlab_old/ /etc_gitlab_new/
echo "Migration complete"
volumeMounts:
- name: old-data
mountPath: /var/opt_gitlab_old
- name: new-data
mountPath: /var/opt_gitlab_new
- name: old-config
mountPath: /etc_gitlab_old
- name: new-config
mountPath: /etc_gitlab_new
volumes:
- name: old-data
persistentVolumeClaim:
claimName: gitlab-data-pvc
- name: new-data
persistentVolumeClaim:
claimName: gitlab-data-longhorn-pvc
- name: old-config
persistentVolumeClaim:
claimName: gitlab-config-pvc
- name: new-config
persistentVolumeClaim:
claimName: gitlab-config-longhorn-pvc
+29
View File
@@ -96,3 +96,32 @@ spec:
requests: requests:
storage: 1Gi storage: 1Gi
volumeName: gitlab-config-pv volumeName: gitlab-config-pv
---
# ─── Neue Longhorn-PVCs zum Migrieren der Daten (dynamisch provisioniert) ─
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitlab-data-longhorn-pvc
namespace: gitlab
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitlab-config-longhorn-pvc
namespace: gitlab
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
+139
View File
@@ -0,0 +1,139 @@
# ─── ServiceAccount ───────────────────────────────────────────────
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab-runner
namespace: gitlab
---
# ─── Role ─────────────────────────────────────────────────────────
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gitlab-runner
namespace: gitlab
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/attach", "pods/log", "secrets", "configmaps", "services"]
verbs: ["get", "list", "watch", "create", "delete", "update", "patch"]
---
# ─── RoleBinding ──────────────────────────────────────────────────
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gitlab-runner
namespace: gitlab
subjects:
- kind: ServiceAccount
name: gitlab-runner
namespace: gitlab
roleRef:
kind: Role
apiGroup: rbac.authorization.k8s.io
name: gitlab-runner
---
# ─── Secret (Runner Authentication Token, GitLab 16+) ────────────
apiVersion: v1
kind: Secret
metadata:
name: gitlab-runner-secret
namespace: gitlab
type: Opaque
stringData:
runner-token: "glrt-3nNma_nEvL1Bq2zc8m5Zu286MQpwOjIKdDozCnU6MTAQ.01.181jg6jja"
---
# ─── ConfigMap (config.toml) ──────────────────────────────────────
apiVersion: v1
kind: ConfigMap
metadata:
name: gitlab-runner-config
namespace: gitlab
data:
config.toml: |
concurrent = 4
check_interval = 10
log_level = "info"
[session_server]
session_timeout = 1800
---
# ─── Deployment ───────────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitlab-runner
namespace: gitlab
labels:
app: gitlab-runner
spec:
replicas: 1
selector:
matchLabels:
app: gitlab-runner
template:
metadata:
labels:
app: gitlab-runner
spec:
serviceAccountName: gitlab-runner
initContainers:
- name: register-runner
image: gitlab/gitlab-runner:latest
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
gitlab-runner register \
--non-interactive \
--url "$CI_SERVER_URL" \
--token "$RUNNER_TOKEN" \
--executor kubernetes \
--kubernetes-namespace gitlab \
--kubernetes-service-account gitlab-runner \
--kubernetes-pull-policy if-not-present \
--kubernetes-privileged true \
--output-limit 4096 \
--kubernetes-cpu-request "100m" \
--kubernetes-cpu-limit "500m" \
--kubernetes-memory-request "256Mi" \
--kubernetes-memory-limit "4Gi"
env:
- name: CI_SERVER_URL
value: "https://gitlab.henryathome.home64.de"
- name: RUNNER_TOKEN
valueFrom:
secretKeyRef:
name: gitlab-runner-secret
key: runner-token
volumeMounts:
- name: runner-config
mountPath: /etc/gitlab-runner
containers:
- name: gitlab-runner
image: gitlab/gitlab-runner:latest
imagePullPolicy: IfNotPresent
command: ["gitlab-runner", "run", "--user=gitlab-runner", "--working-directory=/home/gitlab-runner"]
env:
- name: CI_SERVER_URL
value: "https://gitlab.henryathome.home64.de"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "1000m"
volumeMounts:
- name: runner-config
mountPath: /etc/gitlab-runner
volumes:
- name: runner-config
emptyDir: {}
+4
View File
@@ -18,6 +18,10 @@ stringData:
} }
prometheus_monitoring['enable'] = false prometheus_monitoring['enable'] = false
registry_external_url 'https://registry.henryathome.home64.de'
registry_nginx['listen_port'] = 5050
registry_nginx['listen_https'] = false
# Authentik SSO (OpenID Connect) # Authentik SSO (OpenID Connect)
gitlab_rails['omniauth_enabled'] = true gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect'] gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect']
+6 -2
View File
@@ -17,9 +17,13 @@ spec:
targetPort: 443 targetPort: 443
nodePort: 31443 nodePort: 31443
- name: ssh - name: ssh
port: 31022 port: 22
targetPort: 31022 targetPort: 22
nodePort: 31022 nodePort: 31022
- name: registry
port: 5050
targetPort: 5050
nodePort: 31050
type: NodePort type: NodePort
# --- # ---
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env bash
set -euo pipefail
NAMESPACE="gitlab"
DEPLOYMENT_NAME="gitlab"
JOB_NAME="gitlab-migrate-to-longhorn"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
MANIFEST_DIR="${SCRIPT_DIR}/manifest"
PV_PVC_MANIFEST="${MANIFEST_DIR}/pv-pvc.yaml"
MIGRATION_JOB_MANIFEST="${MANIFEST_DIR}/migrate-to-longhorn.yaml"
SOURCE_DATA_PVC="gitlab-data-pvc"
SOURCE_CONFIG_PVC="gitlab-config-pvc"
TARGET_DATA_PVC="gitlab-data-longhorn-pvc"
TARGET_CONFIG_PVC="gitlab-config-longhorn-pvc"
BIND_TIMEOUT_SECONDS=900
JOB_TIMEOUT_SECONDS=7200
POD_STOP_TIMEOUT_SECONDS=300
ORIGINAL_REPLICAS="1"
SCALED_DOWN="false"
DRY_RUN="false"
AUTO_CONFIRM="false"
info() {
echo "[INFO] $*"
}
warn() {
echo "[WARN] $*" >&2
}
err() {
echo "[ERROR] $*" >&2
exit 1
}
usage() {
cat <<'EOF'
Usage: migrate-to-longhorn.sh [--dry-run] [--yes] [--help]
Options:
--dry-run Show planned actions without changing cluster state.
--yes Skip interactive confirmation before scaling down GitLab.
--help Show this help.
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN="true"
;;
--yes)
AUTO_CONFIRM="true"
;;
--help|-h)
usage
exit 0
;;
*)
err "Unknown argument: $1"
;;
esac
shift
done
}
restore_deployment_on_error() {
if [[ "${SCALED_DOWN}" == "true" ]]; then
warn "Migration failed. Restoring deployment replicas to ${ORIGINAL_REPLICAS}."
kubectl -n "${NAMESPACE}" scale deployment "${DEPLOYMENT_NAME}" --replicas="${ORIGINAL_REPLICAS}" >/dev/null || true
fi
}
on_exit() {
local exit_code=$?
if [[ ${exit_code} -ne 0 ]]; then
restore_deployment_on_error
warn "Script failed with exit code ${exit_code}."
fi
}
trap on_exit EXIT
require_tool() {
local tool="$1"
command -v "${tool}" >/dev/null 2>&1 || err "Required tool not found: ${tool}"
}
require_file() {
local path="$1"
[[ -f "${path}" ]] || err "Required file not found: ${path}"
}
wait_for_pvc_bound() {
local pvc_name="$1"
local timeout="$2"
local elapsed=0
local interval=5
info "Waiting for PVC ${pvc_name} to become Bound..."
while true; do
local phase
phase="$(kubectl -n "${NAMESPACE}" get pvc "${pvc_name}" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
if [[ "${phase}" == "Bound" ]]; then
info "PVC ${pvc_name} is Bound."
return 0
fi
if (( elapsed >= timeout )); then
err "Timeout while waiting for PVC ${pvc_name} to bind."
fi
sleep "${interval}"
elapsed=$((elapsed + interval))
done
}
wait_for_gitlab_pods_stopped() {
local timeout="$1"
local elapsed=0
local interval=5
info "Waiting for pods with label app=gitlab to stop..."
while true; do
local count
count="$(kubectl -n "${NAMESPACE}" get pods -l app=gitlab --no-headers 2>/dev/null | wc -l | tr -d ' ')"
if [[ "${count}" == "0" ]]; then
info "All GitLab pods are stopped."
return 0
fi
if (( elapsed >= timeout )); then
err "Timeout while waiting for GitLab pods to stop."
fi
sleep "${interval}"
elapsed=$((elapsed + interval))
done
}
wait_for_job_complete() {
local timeout="$1"
info "Waiting for migration job ${JOB_NAME} to complete..."
if kubectl -n "${NAMESPACE}" wait --for=condition=complete "job/${JOB_NAME}" --timeout="${timeout}s" >/dev/null; then
info "Migration job completed successfully."
return 0
fi
warn "Migration job did not complete in time or failed."
kubectl -n "${NAMESPACE}" describe job "${JOB_NAME}" || true
kubectl -n "${NAMESPACE}" logs "job/${JOB_NAME}" --tail=-1 || true
err "Migration job failed."
}
validate_prerequisites() {
require_tool kubectl
require_file "${PV_PVC_MANIFEST}"
require_file "${MIGRATION_JOB_MANIFEST}"
kubectl get namespace "${NAMESPACE}" >/dev/null 2>&1 || err "Namespace does not exist: ${NAMESPACE}"
kubectl -n "${NAMESPACE}" get deployment "${DEPLOYMENT_NAME}" >/dev/null 2>&1 || err "Deployment not found: ${DEPLOYMENT_NAME}"
kubectl -n "${NAMESPACE}" get pvc "${SOURCE_DATA_PVC}" >/dev/null 2>&1 || err "Source PVC not found: ${SOURCE_DATA_PVC}"
kubectl -n "${NAMESPACE}" get pvc "${SOURCE_CONFIG_PVC}" >/dev/null 2>&1 || err "Source PVC not found: ${SOURCE_CONFIG_PVC}"
}
confirm_scale_down() {
if [[ "${AUTO_CONFIRM}" == "true" ]]; then
return 0
fi
echo
warn "GitLab deployment ${DEPLOYMENT_NAME} in namespace ${NAMESPACE} will be scaled to 0 during migration."
read -r -p "Type MIGRATE to continue: " confirmation
if [[ "${confirmation}" != "MIGRATE" ]]; then
err "Confirmation failed. Aborted by user."
fi
}
print_dry_run_plan() {
info "Dry-run mode active. No cluster changes will be made."
info "Planned steps:"
info "1) kubectl apply -f ${PV_PVC_MANIFEST}"
info "2) Wait for PVCs ${TARGET_DATA_PVC} and ${TARGET_CONFIG_PVC} to be Bound"
info "3) Scale deployment ${DEPLOYMENT_NAME} to 0"
info "4) Recreate and run job ${JOB_NAME} from ${MIGRATION_JOB_MANIFEST}"
info "5) Wait for job completion and verify target PVCs"
info "6) Scale deployment ${DEPLOYMENT_NAME} back to ${ORIGINAL_REPLICAS}"
info "No PVC switch in deployment is performed by this script."
}
run_migration() {
ORIGINAL_REPLICAS="$(kubectl -n "${NAMESPACE}" get deployment "${DEPLOYMENT_NAME}" -o jsonpath='{.spec.replicas}')"
ORIGINAL_REPLICAS="${ORIGINAL_REPLICAS:-1}"
if [[ "${DRY_RUN}" == "true" ]]; then
print_dry_run_plan
return 0
fi
confirm_scale_down
info "Applying PVC manifest to ensure Longhorn target PVCs exist."
kubectl apply -f "${PV_PVC_MANIFEST}" >/dev/null
wait_for_pvc_bound "${TARGET_DATA_PVC}" "${BIND_TIMEOUT_SECONDS}"
wait_for_pvc_bound "${TARGET_CONFIG_PVC}" "${BIND_TIMEOUT_SECONDS}"
info "Scaling deployment ${DEPLOYMENT_NAME} down to 0 for consistent copy."
kubectl -n "${NAMESPACE}" scale deployment "${DEPLOYMENT_NAME}" --replicas=0 >/dev/null
SCALED_DOWN="true"
wait_for_gitlab_pods_stopped "${POD_STOP_TIMEOUT_SECONDS}"
info "Recreating migration job ${JOB_NAME}."
kubectl -n "${NAMESPACE}" delete job "${JOB_NAME}" --ignore-not-found >/dev/null
kubectl apply -f "${MIGRATION_JOB_MANIFEST}" >/dev/null
info "Streaming migration job logs."
kubectl -n "${NAMESPACE}" logs -f "job/${JOB_NAME}" || true
wait_for_job_complete "${JOB_TIMEOUT_SECONDS}"
info "Quick verification: target PVCs are still Bound."
wait_for_pvc_bound "${TARGET_DATA_PVC}" 30
wait_for_pvc_bound "${TARGET_CONFIG_PVC}" 30
info "Scaling deployment ${DEPLOYMENT_NAME} back to ${ORIGINAL_REPLICAS}."
kubectl -n "${NAMESPACE}" scale deployment "${DEPLOYMENT_NAME}" --replicas="${ORIGINAL_REPLICAS}" >/dev/null
SCALED_DOWN="false"
info "Migration finished successfully."
info "No deployment PVC switch was done by this script."
info "NFS PVC/PV (gitlab-git-pvc / gitlab-git-pv) were not changed."
info "If you want to switch GitLab to Longhorn later, apply the updated deployment manifest manually."
}
main() {
parse_args "$@"
info "Starting GitLab local PVC to Longhorn migration."
if [[ "${DRY_RUN}" == "true" ]]; then
info "Running in dry-run mode."
fi
validate_prerequisites
run_migration
}
main "$@"
+27
View File
@@ -0,0 +1,27 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: gitea-pv-target
spec:
storageClassName: nfs
capacity:
storage: 30Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.178.186
path: /volume1/giteaRepos
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-pvc-target
namespace: gitea
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 30Gi
+98
View File
@@ -0,0 +1,98 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-runner-data
namespace: gitea
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea-runner
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea-runner
template:
metadata:
labels:
app: gitea-runner
spec:
containers:
- name: runner
image: gitea/act_runner:latest
imagePullPolicy: IfNotPresent
env:
- name: GITEA_INSTANCE_URL
value: "http://gitea.gitea.svc.cluster.local"
- name: GITEA_RUNNER_NAME
value: "k3s-runner-1"
- name: GITEA_RUNNER_LABELS
value: "linux-x64:host,ubuntu-latest:docker://catthehacker/ubuntu:act-latest,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04,alpine:docker://alpine:3.20"
- name: GITEA_RUNNER_REGISTRATION_TOKEN
valueFrom:
secretKeyRef:
name: gitea-runner-secret
key: GITEA_RUNNER_REGISTRATION_TOKEN
- name: DOCKER_HOST
value: "unix:///var/run/docker.sock"
command:
- /bin/sh
- -c
args:
- |
set -e
until [ -S /var/run/docker.sock ]; do
echo "Waiting for Docker socket..."
sleep 2
done
if [ -f /data/.runner_labels ] && [ "$(cat /data/.runner_labels)" != "${GITEA_RUNNER_LABELS}" ]; then
rm -f /data/.runner
fi
printf '%s' "${GITEA_RUNNER_LABELS}" > /data/.runner_labels
if [ ! -f /data/.runner ]; then
act_runner register \
--no-interactive \
--instance "${GITEA_INSTANCE_URL}" \
--token "${GITEA_RUNNER_REGISTRATION_TOKEN}" \
--name "${GITEA_RUNNER_NAME}" \
--labels "${GITEA_RUNNER_LABELS}"
fi
exec act_runner daemon
volumeMounts:
- name: runner-data
mountPath: /data
- name: docker-run
mountPath: /var/run
- name: dind
image: docker:27-dind
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
args:
- --insecure-registry=gitea.gitea.svc.cluster.local
env:
- name: DOCKER_TLS_CERTDIR
value: ""
volumeMounts:
- name: docker-lib
mountPath: /var/lib/docker
- name: docker-run
mountPath: /var/run
volumes:
- name: runner-data
persistentVolumeClaim:
claimName: gitea-runner-data
- name: docker-lib
emptyDir: {}
- name: docker-run
emptyDir: {}
+24 -61
View File
@@ -5,65 +5,20 @@ kind: Namespace
metadata: metadata:
name: gitea name: gitea
# PV + PVC: Gitea (NFS) # PVC: PostgreSQL (Longhorn target)
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: gitea-pv
spec:
storageClassName: nfs
capacity:
storage: 30Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.178.166
path: /export/fastData/gitea/repos
--- ---
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: gitea-pvc name: postgres-longhorn-pvc-3g
namespace: gitea namespace: gitea
spec: spec:
storageClassName: nfs storageClassName: longhorn
accessModes:
- ReadWriteMany
resources:
requests:
storage: 30Gi
# PV + PVC: PostgreSQL (NFS)
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
spec:
storageClassName: nfs
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.178.166
path: /export/fastData/gitea/postgres
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: gitea
spec:
storageClassName: nfs
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
resources: resources:
requests: requests:
storage: 10Gi storage: 3Gi
# Deployment: PostgreSQL # Deployment: PostgreSQL
--- ---
@@ -74,6 +29,8 @@ metadata:
namespace: gitea namespace: gitea
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app: postgres app: postgres
@@ -97,14 +54,12 @@ spec:
volumeMounts: volumeMounts:
- name: postgres-storage - name: postgres-storage
mountPath: /var/lib/postgresql/data mountPath: /var/lib/postgresql/data
securityContext: securityContext:
runAsUser: 1001 fsGroup: 999
runAsGroup: 1000
# fsGroup: 1000
volumes: volumes:
- name: postgres-storage - name: postgres-storage
persistentVolumeClaim: persistentVolumeClaim:
claimName: postgres-pvc claimName: postgres-longhorn-pvc-3g
# Service: PostgreSQL # Service: PostgreSQL
--- ---
@@ -138,14 +93,16 @@ spec:
labels: labels:
app: gitea app: gitea
spec: spec:
nodeSelector:
kubernetes.io/hostname: knode1
containers: containers:
- name: gitea - name: gitea
image: gitea/gitea:latest image: gitea/gitea:latest
env: env:
- name: USER_UID - name: USER_UID
value: "1000" value: "1024"
- name: USER_GID - name: USER_GID
value: "1000" value: "100"
- name: GITEA__database__DB_TYPE - name: GITEA__database__DB_TYPE
value: postgres value: postgres
- name: GITEA__database__HOST - name: GITEA__database__HOST
@@ -158,6 +115,10 @@ spec:
value: giteapassword value: giteapassword
- name: GITEA__server__ROOT_URL - name: GITEA__server__ROOT_URL
value: "https://git.henryathome.home64.de" value: "https://git.henryathome.home64.de"
- name: GITEA__server__DOMAIN
value: git.henryathome.home64.de
- name: GITEA__server__PROTOCOL
value: http
- name: GITEA__server__SSH_DOMAIN - name: GITEA__server__SSH_DOMAIN
value: git.henryathome.home64.de value: git.henryathome.home64.de
- name: GITEA__server__START_SSH_SERVER - name: GITEA__server__START_SSH_SERVER
@@ -166,20 +127,22 @@ spec:
value: "32000" value: "32000"
- name: GITEA__server__SSH_PORT - name: GITEA__server__SSH_PORT
value: "32000" value: "32000"
- name: GITEA__packages__ENABLED
value: "true"
- name: GITEA__repository__ROOT
value: /data/gitea/git/repositories
- name: GITEA__lfs__PATH
value: /data/gitea/git/lfs
ports: ports:
- containerPort: 3000 # HTTP - containerPort: 3000 # HTTP
- containerPort: 32000 # SSH - containerPort: 32000 # SSH
volumeMounts: volumeMounts:
- name: gitea-storage - name: gitea-storage
mountPath: /data mountPath: /data
securityContext:
# runAsUser: 1001
# runAsGroup: 1000
# fsGroup: 1000
volumes: volumes:
- name: gitea-storage - name: gitea-storage
persistentVolumeClaim: persistentVolumeClaim:
claimName: gitea-pvc claimName: gitea-pvc-target
# Service: Gitea (inkl. SSH) # Service: Gitea (inkl. SSH)
--- ---
+58
View File
@@ -0,0 +1,58 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: homarr
namespace: homarr
labels:
app: homarr
spec:
replicas: 1
selector:
matchLabels:
app: homarr
template:
metadata:
labels:
app: homarr
spec:
securityContext:
fsGroup: 1000
containers:
- name: homarr
image: ghcr.io/homarr-labs/homarr:latest
imagePullPolicy: Always
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
env:
- name: AUTH_PROVIDERS
value: "credentials,oidc"
- name: AUTH_OIDC_ISSUER
value: "https://authentik.henryathome.home64.de/application/o/homarr/"
- name: AUTH_OIDC_CLIENT_ID
value: "gLJekZnT5uwDXqWoTolP6YyktjdTAPmSAx7EVLcK"
- name: AUTH_OIDC_CLIENT_SECRET
value: "nX9qYyvtIH1PO3FFM13dvvKakv2eovyO9pFKNDYUKF0sycM8UFl0MgGkysqG5irpFsValNb2QkBLUKCRnCIcUt3M6ztCEe4po1Qqfvr0QZHRdH8d21vSHXMMdQmjQ2WN"
- name: AUTH_OIDC_CLIENT_NAME
value: "Authentik"
- name: SECRET_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: homarr-secret
key: SECRET_ENCRYPTION_KEY
ports:
- containerPort: 7575
protocol: TCP
volumeMounts:
- name: homarr-data
mountPath: /appdata
- name: homarr-data
mountPath: /app/data
volumes:
- name: homarr-data
persistentVolumeClaim:
claimName: homarr-pvc
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: homarr-pvc
namespace: homarr
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 5Gi
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: homarr-secret
namespace: homarr
type: Opaque
stringData:
SECRET_ENCRYPTION_KEY: "0fddc6753cb94b4a1dc38f26c52c4d4dbce019237457ede59893fb1a74017512"
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: homarr
namespace: homarr
spec:
type: NodePort
selector:
app: homarr
ports:
- port: 7575
targetPort: 7575
protocol: TCP
nodePort: 30757
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: homarr
+11
View File
@@ -0,0 +1,11 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-2replicas
provisioner: driver.longhorn.io
parameters:
numberOfReplicas: "2"
staleReplicaTimeout: "30"
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
+3 -3
View File
@@ -2,7 +2,7 @@ kind: ConfigMap
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: immich-env name: immich-env
namespace: photoprism namespace: immich
labels: labels:
app: immich app: immich
data: data:
@@ -18,6 +18,6 @@ data:
DISABLE_REVERSE_GEOCODING: "false" DISABLE_REVERSE_GEOCODING: "false"
REVERSE_GEOCODING_PRECISION: "2" REVERSE_GEOCODING_PRECISION: "2"
PUBLIC_LOGIN_PAGE_MESSAGE: "" PUBLIC_LOGIN_PAGE_MESSAGE: ""
PUID: "0" PUID: "1001"
PGID: "0" PGID: "100"
DB_PASSWORD: "password" DB_PASSWORD: "password"
@@ -2,9 +2,11 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: immich-database name: immich-database
namespace: photoprism namespace: immich
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app: immich-database app: immich-database
@@ -13,6 +15,8 @@ spec:
labels: labels:
app: immich-database app: immich-database
spec: spec:
securityContext:
fsGroup: 1000
containers: containers:
- name: immich-postgres - name: immich-postgres
image: "docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0" image: "docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0"
-1
View File
@@ -2,7 +2,6 @@ kind: PersistentVolume
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: immich-db-pv name: immich-db-pv
namespace: photoprism
labels: labels:
app: immich-postgresql app: immich-postgresql
spec: spec:
+4 -5
View File
@@ -2,14 +2,13 @@ kind: PersistentVolumeClaim
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: immich-db-pvc name: immich-db-pvc
namespace: photoprism namespace: immich
labels: labels:
app: immich app: immich
spec: spec:
accessModes: accessModes:
- ReadWriteMany - ReadWriteOnce
resources: resources:
requests: requests:
storage: 20Gi # Match or be less than the PV's capacity storage: 8Gi
volumeName: immich-db-pv # Bind explicitly to the PV created above storageClassName: longhorn
storageClassName: nfs
+1 -1
View File
@@ -2,7 +2,7 @@ kind: Service
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: immich-database name: immich-database
namespace: photoprism namespace: immich
labels: labels:
app: immich-database app: immich-database
spec: spec:
@@ -0,0 +1,16 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: immich-library-immich-pv
labels:
app: immich
spec:
capacity:
storage: 50Gi
storageClassName: nfs
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /export/immichLibrary
server: 192.168.178.166
+1 -2
View File
@@ -2,7 +2,6 @@ kind: PersistentVolume
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: immich-library-pv name: immich-library-pv
namespace: immich
labels: labels:
app: immich app: immich
spec: spec:
@@ -12,5 +11,5 @@ spec:
accessModes: accessModes:
- ReadWriteMany - ReadWriteMany
nfs: nfs:
path: /export/fastData/immichLibrary # Static path on the NFS server path: /export/immichLibrary # Static path on the NFS server
server: 192.168.178.166 server: 192.168.178.166
@@ -2,7 +2,7 @@ kind: PersistentVolumeClaim
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: immich-library-pvc name: immich-library-pvc
namespace: photoprism namespace: immich
labels: labels:
app: immich app: immich
spec: spec:
@@ -11,5 +11,5 @@ spec:
resources: resources:
requests: requests:
storage: 50Gi # Match or be less than the PV's capacity storage: 50Gi # Match or be less than the PV's capacity
volumeName: immich-library-pv # Bind explicitly to the PV created above volumeName: immich-library-immich-pv
storageClassName: nfs storageClassName: nfs
@@ -2,7 +2,7 @@ kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
metadata: metadata:
name: immich-machine-learning name: immich-machine-learning
namespace: photoprism namespace: immich
labels: labels:
app: immich-machine-learning app: immich-machine-learning
spec: spec:
+1 -1
View File
@@ -2,7 +2,7 @@ kind: Service
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: immich-machine-learning name: immich-machine-learning
namespace: photoprism namespace: immich
labels: labels:
app: immich-machine-learning app: immich-machine-learning
spec: spec:
@@ -0,0 +1,19 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: immich-photos-pv
labels:
app: immich
spec:
capacity:
storage: 200Gi
storageClassName: nfs
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.178.186
path: /volume1/Photos
mountOptions:
- hard
- nfsvers=4
@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: immich-photos-pvc
namespace: immich
labels:
app: immich
spec:
storageClassName: nfs
volumeName: immich-photos-pv
accessModes:
- ReadWriteMany
resources:
requests:
storage: 50Gi
@@ -2,7 +2,7 @@ kind: Deployment
apiVersion: apps/v1 apiVersion: apps/v1
metadata: metadata:
name: immich-server name: immich-server
namespace: photoprism namespace: immich
labels: labels:
app: immich-server app: immich-server
spec: spec:
@@ -25,7 +25,8 @@ spec:
}] }]
spec: spec:
securityContext: securityContext:
fsGroup: 0 fsGroup: 100
fsGroupChangePolicy: OnRootMismatch
serviceAccountName: default serviceAccountName: default
dnsPolicy: ClusterFirst dnsPolicy: ClusterFirst
initContainers: initContainers:
@@ -60,7 +61,8 @@ spec:
image: "ghcr.io/immich-app/immich-server:release" image: "ghcr.io/immich-app/immich-server:release"
imagePullPolicy: Always imagePullPolicy: Always
securityContext: securityContext:
runAsUser: 0 runAsUser: 1001
runAsGroup: 100
ports: ports:
- containerPort: 3001 - containerPort: 3001
env: env:
@@ -112,4 +114,4 @@ spec:
claimName: immich-library-pvc claimName: immich-library-pvc
- name: ext-library - name: ext-library
persistentVolumeClaim: persistentVolumeClaim:
claimName: photoprism-library-pvc claimName: immich-photos-pvc
@@ -2,7 +2,7 @@ kind: Service
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: immich-server name: immich-server
namespace: photoprism namespace: immich
labels: labels:
app: immich-server app: immich-server
spec: spec:
@@ -12,4 +12,5 @@ spec:
ports: ports:
- port: 2283 - port: 2283
targetPort: 2283 targetPort: 2283
nodePort: 31139
protocol: TCP protocol: TCP
@@ -0,0 +1,38 @@
apiVersion: batch/v1
kind: Job
metadata:
name: immich-db-nfs-to-longhorn
namespace: immich
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: migrate-db
image: alpine:3.20
command:
- sh
- -c
- |
set -e
apk add --no-cache rsync
test -d /source/postgres
mkdir -p /target/postgres
rsync -aHAX --numeric-ids /source/postgres/ /target/postgres/
test -f /target/postgres/PG_VERSION
test -d /target/postgres/base
volumeMounts:
- name: source-nfs
mountPath: /source
readOnly: true
- name: target-longhorn
mountPath: /target
volumes:
- name: source-nfs
nfs:
server: 192.168.178.166
path: /export/fastData/immichDB
- name: target-longhorn
persistentVolumeClaim:
claimName: immich-db-pvc
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: immich
+2 -2
View File
@@ -9,7 +9,7 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: redis-server name: redis-server
namespace: photoprism namespace: immich
labels: labels:
app: redis-server app: redis-server
spec: spec:
@@ -35,7 +35,7 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: redis-server name: redis-server
namespace: photoprism namespace: immich
labels: labels:
app: redis-server app: redis-server
spec: spec:
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: speedtest-tracker
@@ -0,0 +1,62 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: speedtest-tracker
namespace: speedtest-tracker
labels:
app.kubernetes.io/name: speedtest-tracker
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: speedtest-tracker
template:
metadata:
labels:
app.kubernetes.io/name: speedtest-tracker
spec:
dnsPolicy: None
dnsConfig:
nameservers:
- 10.152.183.10
- 8.8.8.8
searches:
- speedtest-tracker.svc.cluster.local
options:
- name: ndots
value: "5"
containers:
- name: speedtest-tracker
image: lscr.io/linuxserver/speedtest-tracker:latest
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: APP_KEY
valueFrom:
secretKeyRef:
name: speedtest-tracker-secret
key: APP_KEY
- name: DISPLAY_TIMEZONE
value: Europe/Berlin
- name: DB_CONNECTION
value: sqlite
- name: SPEEDTEST_SCHEDULE
value: "0 * * * *"
- name: SPEEDTEST_SERVERS
value: ""
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- mountPath: /config
name: speedtest-tracker
volumes:
- name: speedtest-tracker
persistentVolumeClaim:
claimName: speedtest-tracker
@@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: speedtest-tracker
namespace: speedtest-tracker
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
ingressClassName: traefik
rules:
- host: speedtest.henryathome.home64.de
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: speedtest-tracker
port:
number: 80
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: speedtest-tracker
namespace: speedtest-tracker
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 5Gi
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: speedtest-tracker-secret
namespace: speedtest-tracker
type: Opaque
stringData:
APP_KEY: "base64:kRwkJqieSmtYw0+066zNiNgXInLSexYxT9RgIyONNMI=" # https://speedtest-tracker.dev
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: speedtest-tracker
namespace: speedtest-tracker
labels:
app.kubernetes.io/name: speedtest-tracker
spec:
type: NodePort
selector:
app.kubernetes.io/name: speedtest-tracker
ports:
- port: 80
targetPort: 80
protocol: TCP
nodePort: 30800