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.11-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 # Generous timeout: on an I/O-bound single node even exec can take # seconds, and a 1s default kills a healthy postgres mid-recovery. timeoutSeconds: 5 startupProbe: exec: command: ["pg_isready", "-U", "postgres", "-d", "postgres"] # Crash recovery on an I/O-starved single node can fsync for 10+ # minutes; killing postgres mid-recovery restarts the fsync from # zero and loops forever. 90x10s = 15 minutes of grace. failureThreshold: 90 periodSeconds: 10 livenessProbe: exec: command: ["pg_isready", "-U", "postgres", "-d", "postgres"] initialDelaySeconds: 30 periodSeconds: 20 # Same I/O reasoning as readiness, plus more misses before a kill: # restarting postgres on a loaded node only makes recovery longer. timeoutSeconds: 5 failureThreshold: 5 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "2Gi" cpu: "2000m" 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}" : "${NETBOX_DB_PASSWORD:?NETBOX_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 netbox netbox "$NETBOX_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"