feat(pangolin): Switch Pangolin database from SQLite to PostgreSQL

Deploy a dedicated PostgreSQL 17 instance in the pangolin namespace
and switch Pangolin (fosrl/pangolin) to the postgresql-1.21.1 image
variant. The database URL is provided via a sealed secret and pulled
into the Pangolin container as DATABASE_URL, which the PostgreSQL
Pangolin build reads at startup.

- New: postgres.yaml (Deployment + PVC on hcloud-volumes-encrypted + Service)
- New: pangolin-postgres-secrets-sealed.yaml (postgres creds + connection string)
- pangolin-deployment.yaml: image -> fosrl/pangolin:postgresql-1.21.1,
  DATABASE_URL from secret, /app/config PVC mount, init container waits
  for postgres, removed obsolete DATABASE_PATH env
- pangolin-config.yaml: dropped SQLite database.path stanza

Fixes DEV-451.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO Agent 2026-08-08 11:14:43 +00:00
parent 8215a4f77b
commit 3fff00cfd0
4 changed files with 167 additions and 8 deletions

View file

@ -29,9 +29,6 @@ data:
signup: true signup: true
verification: false verification: false
database:
path: "/data/pangolin.db"
oidc: oidc:
enabled: true enabled: true
issuer: "https://auth.basicstack.de" issuer: "https://auth.basicstack.de"

View file

@ -1,5 +1,5 @@
--- ---
# Pangolin Data PersistentVolumeClaim # Pangolin Data PersistentVolumeClaim (retained for pangolin runtime files at /app/config)
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
@ -33,9 +33,33 @@ spec:
labels: labels:
app: pangolin app: pangolin
spec: spec:
initContainers:
- name: wait-for-postgres
image: postgres:17.5
command:
- /bin/sh
- -c
- |
set -e
until pg_isready -h pangolin-postgres.pangolin.svc.cluster.local -p 5432 -U "$POSTGRES_USER" -d "$POSTGRES_DB"; do
echo "waiting for postgres..."
sleep 2
done
echo "postgres is ready"
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: pangolin-postgres-secrets
key: postgres-user
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: pangolin-postgres-secrets
key: postgres-db
containers: containers:
- name: pangolin - name: pangolin
image: fosrl/pangolin:1.21.1 image: fosrl/pangolin:postgresql-1.21.1
ports: ports:
- containerPort: 3000 - containerPort: 3000
name: api name: api
@ -44,8 +68,12 @@ spec:
env: env:
- name: PANGOLIN_URL - name: PANGOLIN_URL
value: "https://pangolin.basicstack.de" value: "https://pangolin.basicstack.de"
- name: DATABASE_PATH # PostgreSQL database URL (Pangolin PostgreSQL variant reads this at startup)
value: "/data/pangolin.db" - name: DATABASE_URL
valueFrom:
secretKeyRef:
name: pangolin-postgres-secrets
key: connection-string
# OIDC Configuration # OIDC Configuration
- name: OIDC_ENABLED - name: OIDC_ENABLED
value: "true" value: "true"
@ -73,7 +101,7 @@ spec:
key: admin-password key: admin-password
volumeMounts: volumeMounts:
- name: pangolin-data - name: pangolin-data
mountPath: /data mountPath: /app/config
- name: config - name: config
mountPath: /app/config/config.yml mountPath: /app/config/config.yml
subPath: config.yml subPath: config.yml

View file

@ -0,0 +1,18 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: pangolin-postgres-secrets
namespace: pangolin
spec:
encryptedData:
connection-string: AgBxITAqIcO4rqdcbJSRdVJv5TO2teg4tjgXsXeB7hSLxcFTLST/4wQPigaf1flrxMlWejbsmP7n2VXy6KOqwskcCHF/KR+7usGD4xAeIsM1LZDi1NLcFt7RVnR+vhoUcU6+F8xm5Tc6QCZnwfJBEoP2yVeZSIj9XwTUYeluoZF32FD54lQatMphPl4nD7AjLC2SD6i9pB46f5h+zWfYUrtoMk0uNrnxPLpM8YnOXZHl8cLOcXAuGu7iWhQMRfCYv7Nt2/HnDNVVjoZ7qcfTRPF4snjOePBYsxVynmZojtHroGosaFGY/FUWkMEB8j7BCQiV4hlVAAuHpGwSZs37+kDOJY4z1MOriXIqbbQoLvhf8rxArly0TJK+lzuhsu1MsJUPuk/wahcgXI/aN4G98RuJUpxxiUk3EZZ8/IN0E2Kswx59zFhmOjmdxw4eh2oam/Oukc/gaRUs2+Y+/j10JaNQ9yNBCQjCfumw56gNifb1ub6ALC80c2jKGhRaDu8tiG2hWW57VaHFMOuZNQj1a4gcsJNqzTu/oDk1505SiowfQbqJYC21AH9kj/OAEcu0nMAlhSlMpM5tWF/m+VmodSPxq1g3b5/bSJdzXKdQ5WN0PE3LvuneESP3B1YBp58Bb4A4buV3kxpSDAdgmd86uN9g24ZdwxlDWsrFZRYaZh1JnnR84bqqyWwHuElDRpDvO14gpjlD4lNTQfScgJ2z+hmnGddjT7u3atCQu4pFEIzcbg2JVi38Pz5JlwzR1crZfP9tQh/CWrIVOQJI+EQE+MZTczBanlN/o8zf6Q2plqnV0QEDqAVtRwLh5OqsRMPuEXDZ/jJei+N/K0KUhTO26DhDXg==
postgres-db: AgAeCwjNgP6NXncaN7CNMDuI7R6f00qalC08uhy4Kxsc+4twWp+K8x7JGiVosNdirNVRLSrGXaf53Onn2f/uDkgWU8tJETIS/qae1QcxSL3DDtC35XxdymYOPlS+97YBw3Lud0mTVda25v6vnfB5aPiNwXL4u4Nct/eYw15bpVmv2zTb7bNbbdNyu5MSiaA14Wlc/9jMXHnJTeGLEno4FEwzaUBFwEmQRyOE7XQ/z2Fb/47xTXnkn29roe2yHX+25lqC7OWx3F9hVVhUon34u/Y4ybuuUwJix7mx3DB2ObKYV8FC939nqEfNw5vraO6B4ICXPTi/hRXT3kWfRZCQ3XHFoUWU5uD4Skp2d10YarsxGxpLq0ZP/CMD51Pr7jbAYQ9WwbLvhwYHv0CjuqrZrI8ln7Y+CT/5l9zREhbqDSBLkPIUfsJXNEmnN3IYM5yi2LkUTSswgRmJD3ouSZTEMHpo7sFoDCm4xzD8o+H5ImIL3k4mBHjgQvbHD8p0NfP8jeLb3PlVgoF3qUD0l5Ekg/5MdA49LdD6UK0ma5kT/wEzfXiWPOa/xwvv7yhV3qQru62/lFMaYs/S1o0gJrE7MzXvM/0i6JnLehrtP0w7LWieV7vx6/n7DAKt1HV4quXRURXrfihDK+fJSbaLkJveAdqW63eB+wA3fnE5j2pWNN7cjj1tFOHdL1x2WOhO2XVt5bZbL0jji1D4Wg==
postgres-password: AgCBmdJimG15nZc6fTW06PbUgxUzOgaWc53hxXVbkewSX+/of+RGdkwGbB95f5ShVSnApgvptPHiFsi+LD6w3nsI37Yl4/uFbwmeIL9iL2wFQsB8lHpFAq8xg1lUK0bsmNvAfyXVhTwlPXLNk4nSPQbLTekE/hdZmtDo9AtBphZW13OSNl6exEKeYEw7gEDSRu/M7c0OG2cxY4GmPqXNMKkbNh1YVcFnRhOt9pXtMbDlwWj66ua/GNRMEltAfGWiJiuKKXwbyTip0C8SCwsMad411B88oJDneLpccwFxq7v8WKTSYLy8hZJEIphhC11sOs+wXOnuGhWv/QpK3GU3HLrPst2eSBwc9B+7mDhHJI6JrngBNBO/356HuGA2A/AxNeRxMW42jU+I60iQjGiDE/IwpdAtOH/Mu0o48tUn3COhwbf/cyC06gvUirWgSZKE5lsuO73VCn/CE0FvxA78dnSs0otw01mkXnHzRZrcMlglkL+BX+7vNamhaZxuG+ggYlgGscYHXMrhxfWqpBmO9ENYweHDNeHfeY9FlD3jt5GDSF8eKDz5JY3qoB2CLkblyEcPEE1YHfMGPFLPlF7t1C3FTASlp+Iw2UP00vJ9Yg3s9qO3CmMkokWsBzG0hGWETQhU8J90y48xtH+m9rd8rH+IxH5z9z3KlPa4KQ5FpjrsQ7YJ1i9TCCGeu8HR5/9m1Fq7j+uKgqE+UmfK6MFLTcm3hI9Q1SJgtucp9yJpQVNhdg==
postgres-user: AgCIy54/EVmkpFqGSu95+l7wKYLwRALNCh5SC0dC6q6Q1VzVyZSP8ioUmKRyJT8+vuFwke9veXcTWm1fy0N4x64/s+yJO6eHOvJ2wSsKKiqv+m2AjdX2K72lB7CGHfrfYAH/8mHN0PdkF2loJRLCgcbeRoNEoWNiiP1qS2xzPNBMIH5Dpl5nlA7/wr4sZAWUfITl4Cb1sGksxQqFcy9Ua4HxDSBuIYMEx4CI7oifbVOwFXxm1oIWFqO4ANgO61OI5uKZNGLnpXifnuiITaulaCa+48gm8rg+fhvw1XTQLVnP80DfdsaU1rWu6wxZj3+Vs/S/szuLMx3OmR3HsgdYxj1mY70ZfV/r+qe/Fu+FFFc/GFNF01ZQTWrF+IaaNWUolPhm7NtLKxDy6FmF/Vwvt23Q6fA11pduxqnZlS+0kh+TGmX6s9dtNYcyuaEXfG3/5bPE/DvYEKRR8tAyoQxQTVgKs8SttSuwf+g5y1KkhwlkJqYhu+BPTG7fVVbL+UEceF6EeNc+LVZQTcx0SzLFxEIyUFPZktUAnLIft+S9n3jZCetvzch53DC7Cm5epKvovGisaeCNZ6wGisCL6UKxLa3g3YRCT/tgJsL8SyLE6g+AQsdVnyLJ58VdwqSrD7tLge9aYtJZ4YGHJzV9oN7zWEp15j3fLaQjNLKptzRoPf/VFgdYs0+PTXDDl2Y0WGCoXy6ynyr73KleMg==
template:
metadata:
creationTimestamp: null
name: pangolin-postgres-secrets
namespace: pangolin

116
apps/pangolin/postgres.yaml Normal file
View file

@ -0,0 +1,116 @@
---
# PostgreSQL PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pangolin-postgres-data
namespace: pangolin
annotations:
argocd.argoproj.io/sync-options: Delete=false
spec:
accessModes:
- ReadWriteOnce
storageClassName: hcloud-volumes-encrypted
resources:
requests:
storage: 10Gi
---
# PostgreSQL Service (headless-friendly ClusterIP)
apiVersion: v1
kind: Service
metadata:
name: pangolin-postgres
namespace: pangolin
labels:
app: pangolin-postgres
spec:
type: ClusterIP
selector:
app: pangolin-postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
protocol: TCP
---
# PostgreSQL Deployment (single-replica; PVC is RWO)
apiVersion: apps/v1
kind: Deployment
metadata:
name: pangolin-postgres
namespace: pangolin
labels:
app: pangolin-postgres
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: pangolin-postgres
template:
metadata:
labels:
app: pangolin-postgres
spec:
securityContext:
fsGroup: 999
containers:
- name: postgres
image: postgres:17.5
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: pangolin-postgres-secrets
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pangolin-postgres-secrets
key: postgres-password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: pangolin-postgres-secrets
key: postgres-db
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "1000m"
readinessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
volumes:
- name: data
persistentVolumeClaim:
claimName: pangolin-postgres-data