Initial release: self-hosted client photo gallery
ci / docker (push) Successful in 13s

Rust (axum + sqlx) API and worker sharing a Postgres-backed job queue
(SKIP LOCKED, heartbeat, reaper, typed statuses), S3 storage with derived
keys and a fully private bucket, OIDC photographer login with per-request
allowlist checks, client share links with argon2 passwords and lockout,
cookie-based image authorization with sliding expiry, hand-rolled
spec-compliant streaming ZIP downloads with exact Content-Length,
React + Vite gallery frontend, single Docker image, Helm chart for
external S3 + Postgres, and Gitea CI.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-17 13:12:42 +02:00
co-authored by Claude
commit 16d2a56a78
55 changed files with 11962 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
{{- define "photos.fullname" -}}
{{- if contains "photos" .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-photos" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "photos.labels" -}}
app.kubernetes.io/name: photos
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "photos.secretName" -}}
{{- if .Values.secrets.existingSecret -}}
{{- .Values.secrets.existingSecret -}}
{{- else -}}
{{- include "photos.fullname" . -}}
{{- end -}}
{{- end -}}
{{- define "photos.env" -}}
- name: PUBLIC_URL
value: {{ .Values.publicUrl | quote }}
- name: S3_BUCKET
value: {{ .Values.config.s3.bucket | quote }}
- name: S3_REGION
value: {{ .Values.config.s3.region | quote }}
{{- if .Values.config.s3.endpoint }}
- name: S3_ENDPOINT
value: {{ .Values.config.s3.endpoint | quote }}
{{- end }}
- name: S3_FORCE_PATH_STYLE
value: {{ .Values.config.s3.forcePathStyle | quote }}
- name: OIDC_ISSUER
value: {{ .Values.config.oidcIssuer | quote }}
- name: ALLOWED_EMAILS
value: {{ .Values.config.allowedEmails | quote }}
- name: RUST_LOG
value: {{ .Values.config.logLevel | quote }}
{{- end -}}
@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "photos.fullname" . }}-api
labels:
{{- include "photos.labels" . | nindent 4 }}
app.kubernetes.io/component: api
spec:
replicas: {{ .Values.api.replicas }}
selector:
matchLabels:
app.kubernetes.io/name: photos
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: api
template:
metadata:
labels:
{{- include "photos.labels" . | nindent 8 }}
app.kubernetes.io/component: api
spec:
containers:
- name: api
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["server"]
ports:
- name: http
containerPort: 8080
env:
{{- include "photos.env" . | nindent 12 }}
- name: BIND_ADDR
value: "0.0.0.0:8080"
envFrom:
- secretRef:
name: {{ include "photos.secretName" . }}
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 3
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 10
resources:
{{- toYaml .Values.api.resources | nindent 12 }}
@@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "photos.fullname" . }}-worker
labels:
{{- include "photos.labels" . | nindent 4 }}
app.kubernetes.io/component: worker
spec:
replicas: {{ .Values.worker.replicas }}
selector:
matchLabels:
app.kubernetes.io/name: photos
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: worker
template:
metadata:
labels:
{{- include "photos.labels" . | nindent 8 }}
app.kubernetes.io/component: worker
spec:
containers:
- name: worker
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["worker"]
env:
{{- include "photos.env" . | nindent 12 }}
- name: WORKER_CONCURRENCY
value: {{ .Values.worker.concurrency | quote }}
envFrom:
- secretRef:
name: {{ include "photos.secretName" . }}
resources:
{{- toYaml .Values.worker.resources | nindent 12 }}
+33
View File
@@ -0,0 +1,33 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "photos.fullname" . }}
labels:
{{- include "photos.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.ingress.host }}
secretName: {{ .Values.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "photos.fullname" . }}
port:
name: http
{{- end }}
+15
View File
@@ -0,0 +1,15 @@
{{- if not .Values.secrets.existingSecret }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "photos.fullname" . }}
labels:
{{- include "photos.labels" . | nindent 4 }}
stringData:
DATABASE_URL: {{ required "secrets.databaseUrl (or secrets.existingSecret) is required" .Values.secrets.databaseUrl | quote }}
S3_ACCESS_KEY: {{ required "secrets.s3AccessKey is required" .Values.secrets.s3AccessKey | quote }}
S3_SECRET_KEY: {{ required "secrets.s3SecretKey is required" .Values.secrets.s3SecretKey | quote }}
OIDC_CLIENT_ID: {{ required "secrets.oidcClientId is required" .Values.secrets.oidcClientId | quote }}
OIDC_CLIENT_SECRET: {{ required "secrets.oidcClientSecret is required" .Values.secrets.oidcClientSecret | quote }}
SESSION_SECRET: {{ required "secrets.sessionSecret is required" .Values.secrets.sessionSecret | quote }}
{{- end }}
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "photos.fullname" . }}
labels:
{{- include "photos.labels" . | nindent 4 }}
spec:
selector:
app.kubernetes.io/name: photos
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: api
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http