first commit
Docker-Images bauen und veroeffentlichen / build-and-push (push) Failing after 1m32s

This commit is contained in:
Nick Szaunig committed 2026-09-27 06:06:56 +02:00
commit 59bb5a38a3
132 files changed
+19435

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
# Next.js-Anwendung. Zustandslos: Buchungen, Kapazitaet, Gutscheine,
# Reservierungen, Rate Limits, E-Mail-Ausgang und Zahlungsjobs liegen in
# PostgreSQL — beliebig viele Replikate moeglich.
#
# Migrationen laufen NICHT beim Start, sondern als eigener, kontrollierter
# Schritt (jobs.yaml, CronJob "artista-migrate", siehe docs/BETRIEB.md).
apiVersion: apps/v1
kind: Deployment
metadata:
name: artista-app
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
selector:
matchLabels:
app.kubernetes.io/name: artista-app
template:
metadata:
labels:
app.kubernetes.io/name: artista-app
artista/db-client: "true"
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
seccompProfile: { type: RuntimeDefault }
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: artista-app
containers:
- name: app
image: artista-modelcamp
ports:
- name: http
containerPort: 3000
envFrom:
- configMapRef: { name: artista-config }
- secretRef: { name: artista-secrets }
readinessProbe:
httpGet: { path: /api/health/ready, port: http }
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
# Bewusst ohne Datenbank: eine DB-Stoerung soll nicht zu
# Neustartschleifen fuehren.
httpGet: { path: /api/health, port: http }
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
resources:
requests: { cpu: 200m, memory: 256Mi }
limits: { memory: 768Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- name: media
mountPath: /app/media
readOnly: true
# Reiner Zwischenspeicher fuer optimierte Bilder: fluechtig ok.
- name: next-cache
mountPath: /app/.next/cache
- name: tmp
mountPath: /tmp
volumes:
- name: media
persistentVolumeClaim: { claimName: artista-media }
- name: next-cache
emptyDir: { sizeLimit: 512Mi }
- name: tmp
emptyDir: { sizeLimit: 64Mi }
---
apiVersion: v1
kind: Service
metadata:
name: artista-app
spec:
selector:
app.kubernetes.io/name: artista-app
ports:
- name: http
port: 80
targetPort: http
---
# Die App nimmt Verbindungen nur vom Ingress-Controller an. Ohne diese Regel
# koennte jeder Pod im Cluster den Service direkt ansprechen und per
# gefaelschtem X-Real-Ip-Header die Rate Limits (inkl. Admin-Login/2FA)
# umgehen. Namespace des Traefik-Controllers anpassen
# ("kubectl get pods -A | grep traefik").
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: artista-app-ingress
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: artista-app
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
ports:
- port: 3000
# Kubelet-Probes kommen vom Knoten und sind von NetworkPolicies je nach
# CNI (Canal/Calico in RKE2) ausgenommen.
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: artista-app
spec:
minAvailable: 1
selector:
matchLabels:
app.kubernetes.io/name: artista-app
+61
View File
@@ -0,0 +1,61 @@
apiVersion: v1
kind: Namespace
metadata:
name: artista
labels:
# Pod Security Admission: nur eingeschraenkte (non-root) Pods.
pod-security.kubernetes.io/enforce: restricted
---
# Nicht geheime Einstellungen. Geheimnisse: k8s/secret.example.yaml.
apiVersion: v1
kind: ConfigMap
metadata:
name: artista-config
data:
APP_ENV: "production"
APP_URL: "https://modelcamp.example.org"
DATABASE_POOL_SIZE: "10"
MAIL_TRANSPORT: "smtp"
MAIL_FROM: "ARTISTA MODELCAMP <[email protected]>"
ADMIN_NOTIFY_EMAIL: ""
SMTP_PORT: "587"
SMTP_SECURE: "false"
MEDIA_DIR: "/app/media"
EXTERNAL_VIDEO_PROVIDER: "none"
---
# Freigegebene Bilder und Videos (Unterkunft, Portraets, Feedback-Videos).
# Dauerhaft, deshalb ein PersistentVolumeClaim und KEIN emptyDir.
#
# ReadWriteMany, weil mehrere App-Replikate dasselbe Verzeichnis lesen.
# Geeignet: z. B. Longhorn (RWX), NFS oder eine SMB/CSI-Freigabe — fuer
# Medien ist ein Netzwerkdateisystem unkritisch (anders als fuer PostgreSQL).
# Steht nur ReadWriteOnce zur Verfuegung: replicas im Deployment auf 1 setzen.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artista-media
spec:
accessModes: ["ReadWriteMany"]
# storageClassName: longhorn
resources:
requests:
storage: 5Gi
---
# Die Datenbank ist nur fuer Pods mit dem Label artista/db-client erreichbar
# (App, Migration, Worker, Sicherung).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: artista-postgres-access
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: artista-postgres
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
artista/db-client: "true"
ports:
- port: 5432
+159
View File
@@ -0,0 +1,159 @@
# --- Migration ----------------------------------------------------------------
# Als SUSPENDIERTER CronJob definiert, damit Kustomize das Image mit der
# App gleichzieht. Er laeuft nie von selbst, sondern bewusst vor jedem
# Rollout:
# kubectl -n artista create job migrate-$(date +%Y%m%d%H%M) --from=cronjob/artista-migrate
# kubectl -n artista wait --for=condition=complete job/migrate-… --timeout=5m
apiVersion: batch/v1
kind: CronJob
metadata:
name: artista-migrate
spec:
suspend: true
schedule: "0 0 1 1 *"
jobTemplate:
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
artista/db-client: "true"
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1001
seccompProfile: { type: RuntimeDefault }
containers:
- name: migrate
image: artista-modelcamp-tools
command: ["prisma", "migrate", "deploy"]
envFrom:
- secretRef: { name: artista-secrets }
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { memory: 512Mi }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
---
# --- Worker -------------------------------------------------------------------
# Gibt abgelaufene Reservierungen frei (nach Rueckfrage bei Stripe), zieht
# faellige Raten ein, trennt Zahlungsmethoden nach der letzten Rate,
# versendet den E-Mail-Ausgang. Idempotent; Forbid verhindert
# ueberlappende Laeufe zusaetzlich.
apiVersion: batch/v1
kind: CronJob
metadata:
name: artista-worker
spec:
schedule: "*/10 * * * *"
timeZone: "Europe/Berlin"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 900
template:
metadata:
labels:
artista/db-client: "true"
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1001
seccompProfile: { type: RuntimeDefault }
containers:
- name: worker
image: artista-modelcamp-tools
command: ["tsx", "scripts/worker.ts"]
envFrom:
- configMapRef: { name: artista-config }
- secretRef: { name: artista-secrets }
resources:
requests: { cpu: 50m, memory: 192Mi }
limits: { memory: 512Mi }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
---
# --- Sicherung ----------------------------------------------------------------
# Taeglicher pg_dump mit 30 Tagen Aufbewahrung. Das Backup-Volume darf ein
# Netzwerkspeicher (NFS/SMB) sein. Zusaetzlich regelmaessig aus dem Cluster
# kopieren (docs/BETRIEB.md, "Sicherung und Wiederherstellung").
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artista-db-backup
spec:
accessModes: ["ReadWriteOnce"]
# storageClassName: smb-backup
resources:
requests:
storage: 10Gi
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: artista-db-backup
spec:
schedule: "15 3 * * *"
timeZone: "Europe/Berlin"
concurrencyPolicy: Forbid
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 2
template:
metadata:
labels:
artista/db-client: "true"
spec:
restartPolicy: OnFailure
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 70
runAsGroup: 70
fsGroup: 70
seccompProfile: { type: RuntimeDefault }
containers:
- name: pg-dump
# Gleiche Hauptversion wie der Server.
image: postgres:18.1-alpine
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef: { name: artista-secrets, key: DB_PASSWORD }
command: ["/bin/sh", "-c"]
args:
- |
set -eu
set -o pipefail
target="/backup/artista-$(date -u +%Y-%m-%d_%H%M).dump"
# Custom-Format: komprimiert, selektiv wiederherstellbar.
pg_dump --host=artista-postgres --username=artista --dbname=artista \
--format=custom --file="${target}.partial"
mv "${target}.partial" "${target}"
find /backup -name 'artista-*.dump.partial' -mtime +1 -delete
find /backup -name 'artista-*.dump' -mtime +30 -delete
ls -lh /backup
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { memory: 256Mi }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
volumeMounts:
- name: backup
mountPath: /backup
volumes:
- name: backup
persistentVolumeClaim: { claimName: artista-db-backup }
+14
View File
@@ -0,0 +1,14 @@
# Basis fuer RKE2. Nicht direkt anwenden — ueber ein Overlay
# (k8s/overlays/production), das Domain, Images und TLS setzt.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: artista
resources:
- config.yaml
- postgres.yaml
- app.yaml
- jobs.yaml
labels:
- pairs:
app.kubernetes.io/part-of: artista-modelcamp
includeSelectors: false
+82
View File
@@ -0,0 +1,82 @@
# PostgreSQL fuer Buchungen, Zahlungen, Gutscheine und Protokoll.
#
# SPEICHER: Blockspeicher mit ReadWriteOnce verwenden (z. B. Longhorn,
# local-path, Ceph RBD). KEINE SMB/CIFS-Freigabe als Datenverzeichnis:
# SMB bildet POSIX-Sperren, fsync-Garantien und Dateirechte nicht so ab,
# wie PostgreSQL sie fuer Absturzsicherheit voraussetzt — Datenverlust bei
# Stromausfall oder Neustart ist die Folge. Sicherungen (backup-CronJob)
# duerfen dagegen auf SMB/NFS liegen.
#
# Alternative fuer hoehere Verfuegbarkeit: CloudNativePG-Operator; dann
# dieses Manifest weglassen und DATABASE_URL auf dessen Service zeigen lassen.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: artista-postgres
spec:
serviceName: artista-postgres
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: artista-postgres
template:
metadata:
labels:
app.kubernetes.io/name: artista-postgres
spec:
securityContext:
runAsNonRoot: true
runAsUser: 70
runAsGroup: 70
fsGroup: 70
seccompProfile: { type: RuntimeDefault }
containers:
- name: postgres
image: postgres:18.1-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
value: artista
- name: POSTGRES_DB
value: artista
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef: { name: artista-secrets, key: DB_PASSWORD }
volumeMounts:
# Ab PostgreSQL 18 liegt PGDATA unter /var/lib/postgresql/18/docker.
- name: data
mountPath: /var/lib/postgresql
readinessProbe:
exec: { command: ["pg_isready", "-U", "artista"] }
periodSeconds: 5
livenessProbe:
exec: { command: ["pg_isready", "-U", "artista"] }
initialDelaySeconds: 30
periodSeconds: 20
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 1Gi }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
# storageClassName: longhorn
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: artista-postgres
spec:
clusterIP: None
selector:
app.kubernetes.io/name: artista-postgres
ports:
- port: 5432
+64
View File
@@ -0,0 +1,64 @@
# Ingress fuer Traefik mit cert-manager.
# Anpassen: Host (auch in APP_URL, kustomization.yaml), ingressClassName
# ("kubectl get ingressclass"), ClusterIssuer, TLS-Secret-Name.
#
# X-Forwarded-For/X-Real-Ip: Rate Limits verlassen sich darauf, dass Traefik
# vom Client mitgeschickte Werte verwirft. Dafuer im Traefik-EntryPoint
# forwardedHeaders.trustedIPs nur auf echte vorgelagerte Proxys setzen.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: artista
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
tls:
- hosts:
- modelcamp.example.org
secretName: artista-tls
rules:
- host: modelcamp.example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: artista-app
port:
name: http
---
# HTTP -> HTTPS. Entfaellt, wenn Traefik das bereits EntryPoint-weit tut
# (Helm: ports.web.redirections.entryPoint.to=websecure).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: artista-http-redirect
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
traefik.ingress.kubernetes.io/router.middlewares: artista-redirect-https@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: modelcamp.example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: artista-app
port:
name: http
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
spec:
redirectScheme:
scheme: https
permanent: true
@@ -0,0 +1,27 @@
# Produktions-Overlay. Anpassen: Images/Tags, Domain, IngressClass,
# ClusterIssuer und TLS-Secret-Name. Anwenden:
# kubectl apply -k k8s/overlays/production
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: artista
resources:
- ../../base
- ingress.yaml
images:
# Gebaut von .gitea/workflows/docker-build.yml: Git-Tag "v0.1.0" pushen
# ergibt Image-Tag "0.1.0" (Alternativen: "sha-<kurz>", "latest").
# Beide Images immer mit identischem Tag ausrollen.
- name: artista-modelcamp
newName: git.encry.de/nick.rosenloecher/artista-filmcamp
newTag: "0.1.0"
- name: artista-modelcamp-tools
newName: git.encry.de/nick.rosenloecher/artista-filmcamp-tools
newTag: "0.1.0"
patches:
- target: { kind: ConfigMap, name: artista-config }
patch: |-
- op: replace
path: /data/APP_URL
value: https://modelcamp.example.org
+28
View File
@@ -0,0 +1,28 @@
# VORLAGE — nicht mit echten Werten ins Repository einchecken.
# Anlegen z. B. per:
# kubectl -n artista create secret generic artista-secrets \
# --from-literal=DB_PASSWORD='…' \
# --from-literal=DATABASE_URL='postgresql://artista:…@artista-postgres:5432/artista' \
# --from-literal=APP_SECRET="$(openssl rand -base64 48)" \
# --from-literal=BETTER_AUTH_SECRET="$(openssl rand -base64 48)" \
# --from-literal=STRIPE_SECRET_KEY='sk_test_…' \
# --from-literal=STRIPE_WEBHOOK_SECRET='whsec_…' \
# --from-literal=SMTP_HOST='…' --from-literal=SMTP_USER='…' --from-literal=SMTP_PASSWORD='…'
# Besser noch: Sealed Secrets oder External Secrets Operator.
apiVersion: v1
kind: Secret
metadata:
name: artista-secrets
namespace: artista
type: Opaque
stringData:
DB_PASSWORD: "<starkes-passwort>"
# Passwort URL-kodiert einsetzen, falls Sonderzeichen enthalten sind.
DATABASE_URL: "postgresql://artista:<starkes-passwort>@artista-postgres:5432/artista"
APP_SECRET: "<mind-32-zeichen-zufall>"
BETTER_AUTH_SECRET: "<anderer-mind-32-zeichen-zufall>"
STRIPE_SECRET_KEY: "<sk_test_… oder sk_live_…>"
STRIPE_WEBHOOK_SECRET: "<whsec_…>"
SMTP_HOST: "<smtp.example.org>"
SMTP_USER: "<benutzer>"
SMTP_PASSWORD: "<passwort>"