feat: platform backbone and field capture, first part (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), reviewed tasks: - run roles api/worker/job/all with shared startup checks (Task 1) - schema and migration for DomainEvent, deliveries, JobRun, ThrottlerHit (Task 2) - audit, notifications and domain events written in the same transaction (Task 3) - dispatcher with SKIP LOCKED claims, retries and stale release (Task 4) - worker building blocks: tick, LISTEN/NOTIFY wake-up, health (Task 5) - worker process and entry points (Task 6) - throttler counters in Postgres (Task 19) - separate keys per purpose with key ring (Task 20) - public origin page without exact location (Task 22) - shop keys split into read and write (Task 23) - Kustomize base, components, overlay encry, CI validation (Task 26) Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), reviewed tasks: - brood frames, read endpoints, apiary overview and offline pack - network basics, offline queue v3, navigation sheets, form building blocks - new inspection with last-time hints and weather, varroa per day, treatment prefill, apiary page with round - inspection photo (Task 10) is implemented, review pending Not rolled out. Remaining tasks follow in later commits. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
10cbe9bf2d
commit
c01a38835a
340 files changed
+25695
-1808
No files matched your search
@@ -0,0 +1,35 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: apicore-backend-config
|
||||
data:
|
||||
PORT: "3001"
|
||||
JWT_ACCESS_EXPIRES_IN: "15m"
|
||||
JWT_REFRESH_EXPIRES_IN: "7d"
|
||||
FRONTEND_ORIGIN: "https://apicore.example.com"
|
||||
DOCUMENTS_STORAGE_PATH: "/data/documents"
|
||||
# Automatische Backups. Ohne BACKUP_STORAGE_PATH bleibt der Zeitplan
|
||||
# deaktiviert. Bei >1 Replica muss dies auf ein geteiltes Volume zeigen,
|
||||
# wie DOCUMENTS_STORAGE_PATH.
|
||||
BACKUP_STORAGE_PATH: "/data/backups"
|
||||
BACKUP_CRON_SCHEDULE: "0 3 * * *"
|
||||
BACKUP_RETENTION_COUNT: "14"
|
||||
# Optional: groesste einspielbare Backup-Datei in Bytes (Standard 50 MB,
|
||||
# hoechstens 256 MB). Export und Zeitplan warnen, sobald ein Backup
|
||||
# groesser wird. Ein Body-Limit am Ingress muss mitgezogen werden.
|
||||
# BACKUP_MAX_IMPORT_BYTES: "52428800"
|
||||
COOKIE_SECURE: "true"
|
||||
# Client-IP fuer Rate-Limits und IP-Freigaben der API-Schluessel. Vertraut
|
||||
# werden nur das Pod-Netz 10.42.0.0/16 (Traefik) und Cloudflare
|
||||
# (https://www.cloudflare.com/ips/), bewusst nicht LAN/uniquelocal: LAN-Clients
|
||||
# sind selbst Absender, ihre X-Forwarded-For-Werte verwirft Traefik.
|
||||
# Express nimmt den ersten X-Forwarded-For-Eintrag von rechts, der aus keinem
|
||||
# dieser Netze kommt: den Client, den Cloudflare angehaengt hat. Mit "1" waere
|
||||
# es die Cloudflare-Edge, die sich viele Clients teilen.
|
||||
TRUST_PROXY: "loopback, 10.42.0.0/16, 173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22, 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20, 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13, 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22, 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32, 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32"
|
||||
# Entra ID SSO. Issuer ist die v2.0-Metadaten-URL des Tenants,
|
||||
# Redirect-URI muss exakt der im App-Registrierung hinterlegten entsprechen.
|
||||
# Ohne diese vier Variablen (siehe secret.example.yaml fuer Client-ID/-Secret)
|
||||
# bleibt SSO deaktiviert, /auth/sso/status meldet dann enabled: false.
|
||||
OIDC_ISSUER_URL: "https://login.microsoftonline.com/<tenant-id>/v2.0"
|
||||
OIDC_REDIRECT_URI: "https://api.apicore.example.com/api/v1/auth/sso/callback"
|
||||
@@ -0,0 +1,15 @@
|
||||
# Persistenter Speicher fuer Dokumente, Avatare und die verschluesselten
|
||||
# App-Backups (ConfigMap: DOCUMENTS_STORAGE_PATH=/data/documents,
|
||||
# BACKUP_STORAGE_PATH=/data/backups). Im Cluster: StorageClass smb-app
|
||||
# (SMB 3.1.1 mit seal, uid/gid 1000 = User "node" aus dem Dockerfile).
|
||||
# ReadWriteMany, damit mehrere Backend-Replicas dieselben Dateien sehen.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: apicore-data
|
||||
spec:
|
||||
accessModes: [ReadWriteMany]
|
||||
storageClassName: smb-app
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
@@ -0,0 +1,88 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: apicore-backend
|
||||
labels:
|
||||
app: apicore-backend
|
||||
spec:
|
||||
# Ohne HPA genau ein Pod. components/api-hpa entfernt replicas, dann
|
||||
# steuert der HPA (1-2 Pods).
|
||||
replicas: 1
|
||||
# Erst der neue Pod, dann weg mit dem alten: scheitert die Migration im
|
||||
# initContainer (components/backend-roles), laeuft der alte Pod weiter.
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: apicore-backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: apicore-backend
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
volumes:
|
||||
# Dokumente, Avatare und App-Backups (DOCUMENTS_STORAGE_PATH /
|
||||
# BACKUP_STORAGE_PATH unter /data). Ohne Volume landen sie im
|
||||
# Container und sind beim naechsten Neustart weg.
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: apicore-data
|
||||
containers:
|
||||
- name: apicore-backend
|
||||
# Ohne Tag: den Digest setzt das Overlay (images in
|
||||
# overlays/encry/kustomization.yaml), nie :latest - sonst gibt es
|
||||
# kein Rollback und jeder Neustart zieht ungeprueft ein neues Image.
|
||||
image: git.encry.de/nick.rosenloecher/apicore-backend
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
ports:
|
||||
- containerPort: 3001
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: apicore-backend-config
|
||||
- secretRef:
|
||||
name: apicore-backend-secret
|
||||
# startupProbe gibt dem Start bis zu 5 Minuten, ohne dass
|
||||
# liveness/readiness in der Zwischenzeit einen Neustart ausloesen.
|
||||
# Das alte Image fuehrt `prisma migrate deploy` noch im CMD aus, ab
|
||||
# components/backend-roles laeuft die Migration im initContainer.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 3001
|
||||
failureThreshold: 30
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3001
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 3001
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: "0"
|
||||
memory: "0"
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
@@ -0,0 +1,39 @@
|
||||
# Vorlage – keine echten Werte. Kopieren, echte Werte setzen, per `kubectl apply` außerhalb
|
||||
# der Versionskontrolle anwenden (oder ueber Secret-Manager/Sealed-Secrets im Cluster).
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: apicore-backend-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Host ist der In-Cluster-Service aus deploy/k8s/base/postgres/service.yaml.
|
||||
# Anderer Namespace als das Backend? Dann FQDN nutzen:
|
||||
# apicore-postgres.<namespace>.svc.cluster.local
|
||||
DATABASE_URL: "postgresql://apicore:change-me-postgres-password@apicore-postgres:5432/apicore?schema=public"
|
||||
# Mindestens 32 Zeichen (server-seitig erzwungen, Start bricht sonst ab) -
|
||||
# signiert jedes Access-/MFA-/SSO-Token. TOTP- und Webhook-Secrets aus der
|
||||
# Zeit vor den eigenen Schluesseln (unten) sind noch damit verschluesselt:
|
||||
# erst nach dem rekey-Job rotieren. Erzeugen z.B. mit: openssl rand -hex 32
|
||||
JWT_ACCESS_SECRET: "change-me-to-a-random-value-at-least-32-characters-long"
|
||||
JWT_REFRESH_SECRET: "change-me-refresh-secret"
|
||||
# One encryption key per purpose (TOTP secrets, webhook signing secrets,
|
||||
# backup files). Required in production: at least 32 characters each, all
|
||||
# three different from each other and from both JWT secrets.
|
||||
# BACKUP_ENCRYPTION_KEY (with MFA_/WEBHOOK_ENCRYPTION_KEY for MFA and webhook
|
||||
# secrets) is needed to restore backups on another deployment.
|
||||
# Rotation: old value to <NAME>_PREVIOUS, new <NAME>, run the rekey job,
|
||||
# then remove <NAME>_PREVIOUS (backup files are not resealed: keep
|
||||
# BACKUP_ENCRYPTION_KEY_PREVIOUS while older files must restore).
|
||||
MFA_ENCRYPTION_KEY: "change-me-to-a-third-random-value-at-least-32-characters"
|
||||
WEBHOOK_ENCRYPTION_KEY: "change-me-to-a-fourth-random-value-at-least-32-characters"
|
||||
BACKUP_ENCRYPTION_KEY: "change-me-to-another-random-value-at-least-32-characters"
|
||||
# MFA_ENCRYPTION_KEY_PREVIOUS: ""
|
||||
# WEBHOOK_ENCRYPTION_KEY_PREVIOUS: ""
|
||||
# BACKUP_ENCRYPTION_KEY_PREVIOUS: ""
|
||||
# Entra ID App-Registrierung (SSO). Siehe configmap.example.yaml
|
||||
# fuer OIDC_ISSUER_URL/OIDC_REDIRECT_URI. Wird auch fuer die
|
||||
# M365-Kalender-Synchronisation wiederverwendet - dafuer in der
|
||||
# App-Registrierung zusaetzlich die Application-Permission
|
||||
# "Calendars.ReadWrite" hinzufuegen (Admin-Consent noetig).
|
||||
OIDC_CLIENT_ID: "change-me-entra-client-id"
|
||||
OIDC_CLIENT_SECRET: "change-me-entra-client-secret"
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: apicore-backend
|
||||
spec:
|
||||
selector:
|
||||
app: apicore-backend
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3001
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: apicore-frontend-config
|
||||
data:
|
||||
# Server-only env var, read at request time (see src/app/api/runtime-config/route.ts).
|
||||
# Changing this and restarting the pods takes effect immediately - no rebuild needed.
|
||||
API_URL: "https://api.apicore.example.com/api/v1"
|
||||
@@ -0,0 +1,57 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: apicore-frontend
|
||||
labels:
|
||||
app: apicore-frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: apicore-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: apicore-frontend
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: apicore-frontend
|
||||
# Ohne Tag: den Digest setzt das Overlay, siehe Backend.
|
||||
image: git.encry.de/nick.rosenloecher/apicore-frontend
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: apicore-frontend-config
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: "0"
|
||||
memory: "0"
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: apicore-frontend
|
||||
spec:
|
||||
selector:
|
||||
app: apicore-frontend
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,16 @@
|
||||
# Grundlage ohne Cluster-Werte: kein Namespace, keine Hosts, keine Digests.
|
||||
# Die liefert overlays/encry. Die Vorlagen *.example.yaml stehen bewusst nicht
|
||||
# unter resources: Secrets bleiben ausserhalb des Repos, die ConfigMaps mit
|
||||
# echten Werten liegen im Overlay.
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- backend/data-pvc.yaml
|
||||
- backend/deployment.yaml
|
||||
- backend/service.yaml
|
||||
- frontend/deployment.yaml
|
||||
- frontend/service.yaml
|
||||
- postgres/data-pvc.yaml
|
||||
- postgres/statefulset.yaml
|
||||
- postgres/service.yaml
|
||||
- postgres/backup-cronjob.yaml
|
||||
@@ -0,0 +1,76 @@
|
||||
# Tägliche pg_dump-Sicherung der ApiCore-Datenbank auf die Storage Box.
|
||||
# Voraussetzung im Cluster: StorageClass smb-backup (SMB 3.1.1 mit seal),
|
||||
# legt pro PVC einen Ordner unter Server/Backups/<namespace>/ an.
|
||||
# Aufbewahrung: 14 Tage täglich, 8 Wochen sonntags.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: apicore-db-backup
|
||||
spec:
|
||||
accessModes: [ReadWriteMany]
|
||||
storageClassName: smb-backup
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: apicore-db-backup
|
||||
spec:
|
||||
schedule: "5 2 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 1
|
||||
activeDeadlineSeconds: 1800
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: apicore-db-backup
|
||||
db-client: "true"
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
runAsUser: 70
|
||||
runAsGroup: 70
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: backup
|
||||
image: postgres:18.6-alpine
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
stamp=$(date +%Y-%m-%d)
|
||||
kind=daily; [ "$(date +%u)" = "7" ] && kind=weekly
|
||||
mkdir -p /backups/daily /backups/weekly
|
||||
pg_dump -h apicore-postgres -Fc -f /backups/$kind/apicore-$stamp.dump.tmp
|
||||
mv /backups/$kind/apicore-$stamp.dump.tmp /backups/$kind/apicore-$stamp.dump
|
||||
find /backups/daily -name '*.dump' -mtime +14 -delete
|
||||
find /backups/weekly -name '*.dump' -mtime +56 -delete
|
||||
ls -l /backups/$kind | tail -3
|
||||
env:
|
||||
- name: PGUSER
|
||||
valueFrom: {secretKeyRef: {name: apicore-postgres-secret, key: POSTGRES_USER}}
|
||||
- name: PGPASSWORD
|
||||
valueFrom: {secretKeyRef: {name: apicore-postgres-secret, key: POSTGRES_PASSWORD}}
|
||||
- name: PGDATABASE
|
||||
valueFrom: {secretKeyRef: {name: apicore-postgres-secret, key: POSTGRES_DB}}
|
||||
resources:
|
||||
requests: {cpu: "0", memory: "0"}
|
||||
limits: {memory: 512Mi}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities: {drop: [ALL]}
|
||||
volumeMounts:
|
||||
- {name: backups, mountPath: /backups}
|
||||
volumes:
|
||||
- name: backups
|
||||
persistentVolumeClaim: {claimName: apicore-db-backup}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Datenverzeichnis der Datenbank, wie im Cluster: statisch gebunden an das PV
|
||||
# pv-apicore-postgres (SMB 3.1.1 mit seal auf der Storage Box, uid/gid 70,
|
||||
# Retain). Das PV und sein Zugangs-Secret gehoeren zur Cluster-Infrastruktur
|
||||
# und liegen nicht in diesem Repo. storageClassName "" verhindert, dass eine
|
||||
# Default-StorageClass ein neues, leeres Volume anlegt.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: apicore-postgres-data
|
||||
labels:
|
||||
app.kubernetes.io/name: apicore-postgres
|
||||
spec:
|
||||
accessModes: [ReadWriteMany]
|
||||
storageClassName: ""
|
||||
volumeName: pv-apicore-postgres
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
@@ -0,0 +1,11 @@
|
||||
# Vorlage – keine echten Werte. Kopieren, echte Werte setzen, per `kubectl apply` außerhalb
|
||||
# der Versionskontrolle anwenden (oder ueber Secret-Manager/Sealed-Secrets im Cluster).
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: apicore-postgres-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
POSTGRES_USER: "apicore"
|
||||
POSTGRES_PASSWORD: "change-me-postgres-password"
|
||||
POSTGRES_DB: "apicore"
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: apicore-postgres
|
||||
labels:
|
||||
app.kubernetes.io/name: apicore-postgres
|
||||
spec:
|
||||
clusterIP: None
|
||||
selector:
|
||||
app.kubernetes.io/name: apicore-postgres
|
||||
ports:
|
||||
- name: postgres
|
||||
port: 5432
|
||||
targetPort: postgres
|
||||
@@ -0,0 +1,132 @@
|
||||
# PostgreSQL wie im Cluster (uebernommen 2026-10-13 per kubectl get, nur
|
||||
# lesend). Selektor und Volumes eines StatefulSets sind unveraenderlich, und
|
||||
# das Repo darf die Live-Haertung nie zuruecknehmen: Aenderungen hier immer
|
||||
# erst gegen `kubectl -n apicore get sts apicore-postgres -o yaml` pruefen.
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: apicore-postgres
|
||||
labels:
|
||||
app.kubernetes.io/name: apicore-postgres
|
||||
spec:
|
||||
serviceName: apicore-postgres
|
||||
replicas: 1
|
||||
persistentVolumeClaimRetentionPolicy:
|
||||
whenDeleted: Retain
|
||||
whenScaled: Retain
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: apicore-postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: apicore-postgres
|
||||
encry.de/tier: database
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
enableServiceLinks: false
|
||||
terminationGracePeriodSeconds: 120
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: media-gpu
|
||||
operator: NotIn
|
||||
values: ["true"]
|
||||
- weight: 50
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values: [rkem01]
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
topologyKey: kubernetes.io/hostname
|
||||
whenUnsatisfiable: ScheduleAnyway
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
encry.de/tier: database
|
||||
volumes:
|
||||
# Statisches Volume (data-pvc.yaml), keine volumeClaimTemplates.
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: apicore-postgres-data
|
||||
# Root-Dateisystem ist nur lesbar: Socket, /tmp und Shared Memory
|
||||
# liegen in begrenzten emptyDirs.
|
||||
- name: postgres-runtime
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 16Mi
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: 64Mi
|
||||
- name: dshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 256Mi
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:18.6-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: postgres
|
||||
containerPort: 5432
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: apicore-postgres-secret
|
||||
env:
|
||||
# Unterordner statt Mount-Root (Layout von Postgres 18): initdb
|
||||
# verlangt ein leeres Verzeichnis, der Mount-Root ist es nicht immer.
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/18/docker
|
||||
securityContext:
|
||||
# uid/gid 70 = postgres im Alpine-Image, passend zum SMB-Mount
|
||||
# (uid=70,gid=70) des Volumes.
|
||||
runAsUser: 70
|
||||
runAsGroup: 70
|
||||
runAsNonRoot: true
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql
|
||||
- name: postgres-runtime
|
||||
mountPath: /var/run/postgresql
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: dshm
|
||||
mountPath: /dev/shm
|
||||
# Bis zu 10 Minuten Start (Recovery nach Absturz), danach liveness
|
||||
# mit viel Geduld: ein Neustart mitten in einer Recovery schadet mehr.
|
||||
startupProbe:
|
||||
exec:
|
||||
command: ["/bin/sh", "-ec", 'exec pg_isready -q -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"']
|
||||
failureThreshold: 60
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["/bin/sh", "-ec", 'exec pg_isready -q -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"']
|
||||
failureThreshold: 10
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["/bin/sh", "-ec", 'exec pg_isready -q -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"']
|
||||
failureThreshold: 3
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 30m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
Reference in new issue
Block a user