diff --git a/authentik/k8s/config.yaml b/authentik/k8s/config.yaml index f888be8..0d3c4e5 100644 --- a/authentik/k8s/config.yaml +++ b/authentik/k8s/config.yaml @@ -6,6 +6,6 @@ metadata: data: AUTHENTIK_IMAGE: ghcr.io/goauthentik/server AUTHENTIK_TAG: "2025.10.2" - AUTHENTIK_POSTGRESQL__HOST: authentik-postgres-service + AUTHENTIK_POSTGRESQL__HOST: postgres.database.svc.cluster.local AUTHENTIK_POSTGRESQL__NAME: authentik AUTHENTIK_ERROR_REPORTING__ENABLED: "true" diff --git a/authentik/k8s/postgresql.yaml b/authentik/k8s/postgresql.yaml deleted file mode 100644 index 9199cb3..0000000 --- a/authentik/k8s/postgresql.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: authentik-postgres-service - namespace: authentik -spec: - clusterIP: None - selector: - app: authentik-postgres - ports: - - port: 5432 - targetPort: 5432 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: authentik-postgres-statefulset - namespace: authentik -spec: - selector: - matchLabels: - app: authentik-postgres - serviceName: authentik-postgres-service - replicas: 1 - template: - metadata: - labels: - app: authentik-postgres - spec: - containers: - - name: postgres - image: docker.io/library/postgres:15.19-alpine - env: - - name: POSTGRES_DB - value: authentik - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: authentik-secrets - key: AUTHENTIK_POSTGRESQL__USER - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: authentik-secrets - key: AUTHENTIK_POSTGRESQL__PASSWORD - ports: - - containerPort: 5432 - name: postgres - volumeMounts: - - name: postgres-data - mountPath: /var/lib/postgresql/data - resources: - requests: - memory: "256Mi" - cpu: "200m" - limits: - memory: "1Gi" - cpu: "500m" - volumeClaimTemplates: - - metadata: - name: postgres-data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 5Gi diff --git a/gitea/k8s/config.yaml b/gitea/k8s/config.yaml index 20b4706..9eb7297 100644 --- a/gitea/k8s/config.yaml +++ b/gitea/k8s/config.yaml @@ -10,13 +10,13 @@ data: GITEA__server__SSH_PORT: "2221" GITEA__database__DB_TYPE: "postgres" - GITEA__database__HOST: "gitea-postgres-service:5432" + GITEA__database__HOST: "postgres.database.svc.cluster.local:5432" GITEA__database__NAME: "gitea" GITEA__security__REVERSE_PROXY_LIMIT: "1" GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: "*" GITEA__mailer__ENABLED: "false" - GITEA__log__logger__access__MODE: "console, file" + GITEA__log__logger.access.MODE: "console, file" USER_UID: "1000" USER_GID: "1000" diff --git a/gitea/k8s/postgres.yaml b/gitea/k8s/postgres.yaml deleted file mode 100644 index 90a1cc6..0000000 --- a/gitea/k8s/postgres.yaml +++ /dev/null @@ -1,62 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: gitea-postgres-service - namespace: gitea -spec: - clusterIP: None - selector: - app: gitea-postgres - ports: - - port: 5432 - targetPort: 5432 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: gitea-postgres-statefulset - namespace: gitea -spec: - selector: - matchLabels: - app: gitea-postgres - serviceName: gitea-postgres-service - replicas: 1 - template: - metadata: - labels: - app: gitea-postgres - spec: - containers: - - name: gitea-postgres - image: postgres:14.24-alpine - env: - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: gitea-secrets - key: GITEA__database__USER - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: gitea-secrets - key: GITEA__database__PASSWD - - name: POSTGRES_DB - valueFrom: - secretKeyRef: - name: gitea-secrets - key: GITEA__database__NAME - ports: - - containerPort: 5432 - name: postgres - volumeMounts: - - name: postgres-data - mountPath: /var/lib/postgresql/data - volumeClaimTemplates: - - metadata: - name: postgres-data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi diff --git a/netronome/k8s/config.yaml b/netronome/k8s/config.yaml index fa4f125..23ac87a 100644 --- a/netronome/k8s/config.yaml +++ b/netronome/k8s/config.yaml @@ -5,7 +5,7 @@ metadata: namespace: netronome data: NETRONOME__DB_TYPE: "postgres" - NETRONOME__DB_HOST: "netronome-postgres-service" + NETRONOME__DB_HOST: "postgres.database.svc.cluster.local" NETRONOME__DB_PORT: "5432" NETRONOME__DB_NAME: "netronome" NETRONOME__DB_SSLMODE: "disable" diff --git a/netronome/k8s/postgres.yaml b/netronome/k8s/postgres.yaml deleted file mode 100644 index 1c3a2e8..0000000 --- a/netronome/k8s/postgres.yaml +++ /dev/null @@ -1,71 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: netronome-postgres-service - namespace: netronome -spec: - selector: - app: netronome-postgres - ports: - - protocol: TCP - port: 5432 - targetPort: 5432 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: netronome-postgres - namespace: netronome -spec: - serviceName: "netronome-postgres-service" - replicas: 1 - selector: - matchLabels: - app: netronome-postgres - template: - metadata: - labels: - app: netronome-postgres - spec: - containers: - - name: netronome-postgres - image: postgres:17.11-alpine - ports: - - name: postgres-port - protocol: TCP - containerPort: 5432 - env: - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: netronome-secrets - key: NETRONOME__DB_USER - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: netronome-secrets - key: NETRONOME__DB_PASSWORD - - name: POSTGRES_DB - valueFrom: - configMapKeyRef: - name: netronome-config - key: NETRONOME__DB_NAME - resources: - requests: - memory: "512Mi" - cpu: "500m" - limits: - memory: "1Gi" - cpu: "1000m" - volumeMounts: - - name: netronome-pg-data - mountPath: /var/lib/postgresql/data - volumeClaimTemplates: - - metadata: - name: netronome-pg-data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi diff --git a/postgres/.env.example b/postgres/.env.example new file mode 100644 index 0000000..ae8600c --- /dev/null +++ b/postgres/.env.example @@ -0,0 +1,5 @@ +POSTGRES_ADMIN_PASSWORD= +AUTHENTIK_DB_PASSWORD= +GITEA_DB_PASSWORD= +NETRONOME_DB_PASSWORD= +PENPOT_DB_PASSWORD= diff --git a/postgres/README.md b/postgres/README.md new file mode 100644 index 0000000..8e2fa7e --- /dev/null +++ b/postgres/README.md @@ -0,0 +1,35 @@ +# Shared PostgreSQL + +This directory contains a PostgreSQL 15 deployment draft for Authentik, Gitea, +Netronome, and Penpot. It creates one database and one login role per service; +it does not migrate existing data or change application connection settings. + +## Compatibility baseline + +| Service | Current application | Current standalone PostgreSQL | Common PostgreSQL 15 | +| --------- | ------------------- | ----------------------------: | ------------------------------------------------------------------------------------ | +| Authentik | 2025.10.2 | 15 | Supported (Authentik requires 14+) | +| Gitea | 1.27.3 | 14 | Supported (Gitea requires 12+) | +| Netronome | 0.14.0 | 17 | Validate in staging; upstream's example uses 17 but no 17-only feature is documented | +| Penpot | 2.17.2 | 15 | Supported by the official deployment | + +PostgreSQL 15 is the conservative common major. A major-version downgrade or +change must use a logical dump/restore; changing only the image tag while +keeping a data directory is not supported. Back up and migrate one application +at a time, starting with Netronome because its current standalone deployment +uses PostgreSQL 17. + +For Compose, copy `.env.example` to `.env`, set all passwords, and start it with +`docker compose -f shared-compose.yaml up -d`. This file is intentionally not +named `compose.yaml`, so the repository deploy workflow does not start a second +database accidentally. +Applications that use this database must also join that external network and use +`homelab-postgres:5432`. + +For Kubernetes, create `k8s/secrets.yaml` from the example before applying the +manifests. The `k8s/active` marker makes the normal deploy workflow include the +namespace, StatefulSet, ConfigMap, and NetworkPolicy. Applications use +`postgres.database.svc.cluster.local:5432`. +Migrate each existing database with a tested logical dump/restore before +switching an application. Do not reuse a PostgreSQL 14 or 17 data directory +with PostgreSQL 15. diff --git a/postgres/initdb/01-create-databases.sh b/postgres/initdb/01-create-databases.sh new file mode 100755 index 0000000..ffdb01d --- /dev/null +++ b/postgres/initdb/01-create-databases.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${AUTHENTIK_DB_PASSWORD:?AUTHENTIK_DB_PASSWORD is required}" +: "${GITEA_DB_PASSWORD:?GITEA_DB_PASSWORD is required}" +: "${NETRONOME_DB_PASSWORD:?NETRONOME_DB_PASSWORD is required}" +: "${PENPOT_DB_PASSWORD:?PENPOT_DB_PASSWORD is required}" + +create_role_and_database() { + local role="$1" + local database="$2" + local password="$3" + + psql --username "$POSTGRES_USER" --dbname postgres \ + -v role="$role" -v database="$database" -v password="$password" \ + <<'SQL' +SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'role', :'password') +WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'role')\gexec +SELECT format('CREATE DATABASE %I OWNER %I', :'database', :'role') +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = :'database')\gexec +SQL +} + +create_role_and_database authentik authentik "$AUTHENTIK_DB_PASSWORD" +create_role_and_database gitea gitea "$GITEA_DB_PASSWORD" +create_role_and_database netronome netronome "$NETRONOME_DB_PASSWORD" +create_role_and_database penpot penpot "$PENPOT_DB_PASSWORD" diff --git a/postgres/k8s/active b/postgres/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/postgres/k8s/namespace.yaml b/postgres/k8s/namespace.yaml new file mode 100644 index 0000000..2b540ae --- /dev/null +++ b/postgres/k8s/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: database + labels: + app.kubernetes.io/part-of: homelab-database diff --git a/postgres/k8s/network-policy.yaml b/postgres/k8s/network-policy.yaml new file mode 100644 index 0000000..b4ec4dd --- /dev/null +++ b/postgres/k8s/network-policy.yaml @@ -0,0 +1,31 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: postgres-ingress + namespace: database +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: postgres17 + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: authentik + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: gitea + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: netronome + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: penpot + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: statuspage + ports: + - protocol: TCP + port: 5432 diff --git a/postgres/k8s/postgres.yaml b/postgres/k8s/postgres.yaml new file mode 100644 index 0000000..886fad4 --- /dev/null +++ b/postgres/k8s/postgres.yaml @@ -0,0 +1,126 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: database + labels: + app.kubernetes.io/name: postgres17 + app.kubernetes.io/part-of: homelab-database +spec: + selector: + app.kubernetes.io/name: postgres17 + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres17 + namespace: database +spec: + serviceName: postgres17 + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: postgres17 + template: + metadata: + labels: + app.kubernetes.io/name: postgres17 + spec: + containers: + - name: postgres + image: postgres:17.6-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: POSTGRES_DB + value: postgres + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-shared-secrets + key: POSTGRES_ADMIN_PASSWORD + envFrom: + - secretRef: + name: postgres-shared-secrets + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + - name: initdb + mountPath: /docker-entrypoint-initdb.d/01-create-databases.sh + subPath: 01-create-databases.sh + readinessProbe: + exec: + command: ["pg_isready", "-U", "postgres", "-d", "postgres"] + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + exec: + command: ["pg_isready", "-U", "postgres", "-d", "postgres"] + initialDelaySeconds: 30 + periodSeconds: 20 + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: postgres17-data + - name: initdb + configMap: + name: postgres-initdb + defaultMode: 0755 +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres17-data + namespace: database + labels: + app.kubernetes.io/name: postgres17 + app.kubernetes.io/part-of: homelab-database +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: local-path-retain + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-initdb + namespace: database +data: + 01-create-databases.sh: | + #!/usr/bin/env bash + set -euo pipefail + + : "${AUTHENTIK_DB_PASSWORD:?AUTHENTIK_DB_PASSWORD is required}" + : "${GITEA_DB_PASSWORD:?GITEA_DB_PASSWORD is required}" + : "${NETRONOME_DB_PASSWORD:?NETRONOME_DB_PASSWORD is required}" + : "${PENPOT_DB_PASSWORD:?PENPOT_DB_PASSWORD is required}" + : "${STATUSPAGE_DB_PASSWORD:?STATUSPAGE_DB_PASSWORD is required}" + + create_role_and_database() { + local role="$1" + local database="$2" + local password="$3" + psql --username "$POSTGRES_USER" --dbname postgres \ + -v role="$role" -v database="$database" -v password="$password" \ + <<'SQL' + SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'role', :'password') + WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'role')\gexec + SELECT format('CREATE DATABASE %I OWNER %I', :'database', :'role') + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = :'database')\gexec + SQL + } + + create_role_and_database authentik authentik "$AUTHENTIK_DB_PASSWORD" + create_role_and_database gitea gitea "$GITEA_DB_PASSWORD" + create_role_and_database netronome netronome "$NETRONOME_DB_PASSWORD" + create_role_and_database penpot penpot "$PENPOT_DB_PASSWORD" + create_role_and_database statuspage statuspage "$STATUSPAGE_DB_PASSWORD" diff --git a/postgres/k8s/secrets.yaml.example b/postgres/k8s/secrets.yaml.example new file mode 100644 index 0000000..4768d78 --- /dev/null +++ b/postgres/k8s/secrets.yaml.example @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres-shared-secrets + namespace: database +type: Opaque +stringData: + POSTGRES_ADMIN_PASSWORD: "" + AUTHENTIK_DB_PASSWORD: "" + GITEA_DB_PASSWORD: "" + NETRONOME_DB_PASSWORD: "" + PENPOT_DB_PASSWORD: "" + STATUSPAGE_DB_PASSWORD: "" diff --git a/postgres/shared-compose.yaml b/postgres/shared-compose.yaml new file mode 100644 index 0000000..872078d --- /dev/null +++ b/postgres/shared-compose.yaml @@ -0,0 +1,28 @@ +services: + postgres: + image: postgres:15.19-alpine + container_name: homelab-postgres + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD:?set POSTGRES_ADMIN_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + - ./initdb:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - database + +volumes: + postgres-data: + +networks: + database: + name: homelab-database