From 52e1f50a8061d85ddc663688655a2f149e4d085b Mon Sep 17 00:00:00 2001 From: mr-forust Date: Mon, 14 Sep 2026 10:25:45 +0200 Subject: [PATCH] feat(postgres): add shared database deployments --- postgres/.env.example | 5 ++ postgres/README.md | 35 ++++++++ postgres/initdb/01-create-databases.sh | 27 ++++++ postgres/k8s/active | 0 postgres/k8s/namespace.yaml | 6 ++ postgres/k8s/network-policy.yaml | 28 ++++++ postgres/k8s/postgres.yaml | 114 +++++++++++++++++++++++++ postgres/k8s/secrets.yaml.example | 12 +++ postgres/shared-compose.yaml | 28 ++++++ 9 files changed, 255 insertions(+) create mode 100644 postgres/.env.example create mode 100644 postgres/README.md create mode 100755 postgres/initdb/01-create-databases.sh create mode 100644 postgres/k8s/active create mode 100644 postgres/k8s/namespace.yaml create mode 100644 postgres/k8s/network-policy.yaml create mode 100644 postgres/k8s/postgres.yaml create mode 100644 postgres/k8s/secrets.yaml.example create mode 100644 postgres/shared-compose.yaml 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..abbc43f --- /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..3877836 --- /dev/null +++ b/postgres/k8s/network-policy.yaml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: postgres-ingress + namespace: database +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: postgres + 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 + ports: + - protocol: TCP + port: 5432 diff --git a/postgres/k8s/postgres.yaml b/postgres/k8s/postgres.yaml new file mode 100644 index 0000000..6a605ba --- /dev/null +++ b/postgres/k8s/postgres.yaml @@ -0,0 +1,114 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: database + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/part-of: homelab-database +spec: + selector: + app.kubernetes.io/name: postgres + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: database +spec: + serviceName: postgres + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: postgres + template: + metadata: + labels: + app.kubernetes.io/name: postgres + spec: + containers: + - name: postgres + image: postgres:15.19-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: initdb + configMap: + name: postgres-initdb + defaultMode: 0755 + volumeClaimTemplates: + - metadata: + name: postgres-data + spec: + accessModes: ["ReadWriteOnce"] + 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}" + + 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/secrets.yaml.example b/postgres/k8s/secrets.yaml.example new file mode 100644 index 0000000..d905da1 --- /dev/null +++ b/postgres/k8s/secrets.yaml.example @@ -0,0 +1,12 @@ +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: "" 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