Files
homelab-manifests/workout-system/workout.yaml
T
2026-04-17 19:40:14 +02:00

651 lines
21 KiB
YAML

# wger - Workout Manager
# https://github.com/wger-project/wger
# Version: 2.5 (official image, no custom fork)
# Domain: workout.dooplex.hu
# Auth: Authentik Forward Auth (domain mode) + native wger AUTH_PROXY middleware
#
# ============================================================================
# MIGRATION NOTES (from 2.3 + custom OIDC fork):
# - Image switched from ghcr.io/kisfenyo/wger-oidc:latest -> wger/server:2.5
# - All OIDC_* / ENABLE_OIDC env vars removed
# - Native AUTH_PROXY_* env vars added (wger 2.4+ feature, PR #1859)
# - Ingress split into two resources:
# * wger -> path / -> protected by Authentik forward-auth
# * wger-api -> path /api/ -> unprotected (JWT auth for mobile app)
# - nginx sidecar: strips client-supplied X-Authentik-* on /api/ (defense in depth)
# - Authentik: create a new Proxy Provider (Forward auth, single application)
# External Host: https://workout.dooplex.hu
# Attach to existing outpost. The old OIDC provider can be deleted.
#
# POST-UPGRADE COMMANDS (run once after rollout stabilises):
# kubectl exec -n workout-system deploy/wger -c wger -- \
# python manage.py recalculate_statistics --all --active-only
# kubectl exec -n workout-system deploy/wger -c wger -- \
# python manage.py evaluate_trophies --all
# ============================================================================
---
apiVersion: v1
kind: Namespace
metadata:
name: workout-system
labels:
app.kubernetes.io/name: wger
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wger-redis
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-redis
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-redis
template:
metadata:
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-redis
spec:
containers:
- name: redis
image: redis:7.2-alpine
ports:
- containerPort: 6379
name: redis
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wger
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger
annotations:
# Track upstream wger releases
extensions.v1alpha1.version-checker.io/wger: "true"
extensions.v1alpha1.version-checker.io/wger.match-regex: "^\\d+\\.\\d+$"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger
spec:
# Prevent k8s from injecting WGER_PORT / WGER_SERVICE_* env vars
# from the wger Service — they collide with wger's own $WGER_PORT
# config and break the startup script (URI instead of port number).
enableServiceLinks: false
securityContext:
fsGroup: 1000
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
name: http
volumeMounts:
- name: static
mountPath: /home/wger/static
readOnly: true
- name: media
mountPath: /home/wger/media
readOnly: true
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: nginx.conf
- name: wger
image: wger/server:2.5
imagePullPolicy: IfNotPresent
env:
# Django settings
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: wger-app
key: secret-key
- name: SIGNING_KEY
valueFrom:
secretKeyRef:
name: wger-app
key: signing-key
- name: DJANGO_DEBUG
value: "False"
- name: WGER_INSTANCE
value: "https://workout.dooplex.hu"
- name: TIME_ZONE
value: "Europe/Budapest"
- name: DJANGO_CACHE_TIMEOUT
value: "120"
- name: CSRF_TRUSTED_ORIGINS
value: "https://workout.dooplex.hu"
# Database (shared CNPG)
- name: DJANGO_DB_ENGINE
value: "django.db.backends.postgresql"
- name: DJANGO_DB_HOST
value: "postgresql-rw.database-system.svc.cluster.local"
- name: DJANGO_DB_PORT
value: "5432"
- name: DJANGO_DB_DATABASE
value: "wger"
- name: DJANGO_DB_USER
valueFrom:
secretKeyRef:
name: wger-db
key: username
- name: DJANGO_DB_PASSWORD
valueFrom:
secretKeyRef:
name: wger-db
key: password
# Cache
- name: DJANGO_CACHE_BACKEND
value: "django_redis.cache.RedisCache"
- name: DJANGO_CACHE_LOCATION
value: "redis://wger-redis:6379/1"
- name: DJANGO_CACHE_CLIENT_CLASS
value: "django_redis.client.DefaultClient"
# Celery
- name: CELERY_BROKER
value: "redis://wger-redis:6379/2"
- name: CELERY_BACKEND
value: "redis://wger-redis:6379/2"
# ----------------------------------------------------------------
# Native Authentication Proxy (wger 2.4+) - replaces OIDC fork
# ----------------------------------------------------------------
- name: AUTH_PROXY_ENABLED
value: "True"
# Django META key format: HTTP_ + uppercase header with - replaced by _
# So X-Authentik-Username => HTTP_X_AUTHENTIK_USERNAME
- name: AUTH_PROXY_HEADER
value: "HTTP_X_AUTHENTIK_USERNAME"
- name: AUTH_PROXY_CREATE_UNKNOWN_USER
value: "True"
- name: AUTH_PROXY_EMAIL_HEADER
value: "HTTP_X_AUTHENTIK_EMAIL"
- name: AUTH_PROXY_NAME_HEADER
value: "HTTP_X_AUTHENTIK_NAME"
# Only trust the auth header when coming from the nginx sidecar
# (same pod, proxies from 127.0.0.1 to Django on :8000).
# This prevents header-spoofing attacks from anywhere else.
- name: AUTH_PROXY_TRUSTED_IPS
value: "127.0.0.1/32"
# Email (disabled - no email sending)
- name: ENABLE_EMAIL
value: "False"
# Media settings
- name: DJANGO_MEDIA_ROOT
value: "/home/wger/media"
- name: DJANGO_STATIC_ROOT
value: "/home/wger/static"
# Features
- name: ALLOW_REGISTRATION
value: "False"
- name: ALLOW_GUEST_USERS
value: "False"
- name: ALLOW_UPLOAD_VIDEOS
value: "True"
- name: USE_RECAPTCHA
value: "False"
- name: DOWNLOAD_EXERCISE_IMAGES_ON_STARTUP
value: "True"
ports:
- containerPort: 8000
name: http
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
volumeMounts:
- name: media
mountPath: /home/wger/media
- name: static
mountPath: /home/wger/static
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 120
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 60
periodSeconds: 10
volumes:
- name: nginx-config
configMap:
name: wger-nginx-config
- name: media
persistentVolumeClaim:
claimName: wger-media
- name: static
persistentVolumeClaim:
claimName: wger-static
---
# Celery worker for background tasks
apiVersion: apps/v1
kind: Deployment
metadata:
name: wger-celery-worker
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-celery-worker
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-celery-worker
template:
metadata:
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-celery-worker
spec:
enableServiceLinks: false
securityContext:
fsGroup: 1000
containers:
- name: celery-worker
image: wger/server:2.5
imagePullPolicy: IfNotPresent
command: ["/start-worker"]
env:
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: wger-app
key: secret-key
- name: SIGNING_KEY
valueFrom:
secretKeyRef:
name: wger-app
key: signing-key
- name: TIME_ZONE
value: "Europe/Budapest"
- name: DJANGO_DB_ENGINE
value: "django.db.backends.postgresql"
- name: DJANGO_DB_HOST
value: "postgresql-rw.database-system.svc.cluster.local"
- name: DJANGO_DB_PORT
value: "5432"
- name: DJANGO_DB_DATABASE
value: "wger"
- name: DJANGO_DB_USER
valueFrom:
secretKeyRef:
name: wger-db
key: username
- name: DJANGO_DB_PASSWORD
valueFrom:
secretKeyRef:
name: wger-db
key: password
- name: DJANGO_CACHE_TIMEOUT
value: "120"
- name: DJANGO_CACHE_CLIENT_CLASS
value: "django_redis.client.DefaultClient"
- name: CELERY_BROKER
value: "redis://wger-redis:6379/2"
- name: CELERY_BACKEND
value: "redis://wger-redis:6379/2"
- name: DJANGO_CACHE_BACKEND
value: "django_redis.cache.RedisCache"
- name: DJANGO_CACHE_LOCATION
value: "redis://wger-redis:6379/1"
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
---
# Celery beat for scheduled tasks
apiVersion: apps/v1
kind: Deployment
metadata:
name: wger-celery-beat
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-celery-beat
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-celery-beat
template:
metadata:
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-celery-beat
spec:
enableServiceLinks: false
securityContext:
fsGroup: 1000
containers:
- name: celery-beat
image: wger/server:2.5
imagePullPolicy: IfNotPresent
command: ["/start-beat"]
env:
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: wger-app
key: secret-key
- name: SIGNING_KEY
valueFrom:
secretKeyRef:
name: wger-app
key: signing-key
- name: TIME_ZONE
value: "Europe/Budapest"
- name: DJANGO_CACHE_TIMEOUT
value: "120"
- name: DJANGO_CACHE_CLIENT_CLASS
value: "django_redis.client.DefaultClient"
- name: DJANGO_DB_ENGINE
value: "django.db.backends.postgresql"
- name: DJANGO_DB_HOST
value: "postgresql-rw.database-system.svc.cluster.local"
- name: DJANGO_DB_PORT
value: "5432"
- name: DJANGO_DB_DATABASE
value: "wger"
- name: DJANGO_DB_USER
valueFrom:
secretKeyRef:
name: wger-db
key: username
- name: DJANGO_DB_PASSWORD
valueFrom:
secretKeyRef:
name: wger-db
key: password
- name: CELERY_BROKER
value: "redis://wger-redis:6379/2"
- name: CELERY_BACKEND
value: "redis://wger-redis:6379/2"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: wger-redis
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-redis
spec:
type: ClusterIP
ports:
- name: redis
port: 6379
targetPort: redis
selector:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-redis
---
apiVersion: v1
kind: Service
metadata:
name: wger
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 80
selector:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger
---
# ============================================================================
# Ingress #1: web UI paths (/) - Authentik forward-auth protected
# ============================================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wger
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
external-dns.alpha.kubernetes.io/hostname: workout.dooplex.hu,workout.home
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
# Authentik Forward Auth (domain mode) - same pattern as your other SSO apps
# If you use an internal outpost service URL elsewhere, swap auth-url for it.
nginx.ingress.kubernetes.io/auth-url: http://ak-outpost-kisfenyo-outpost.auth-system.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx
nginx.ingress.kubernetes.io/auth-signin: https://kisfenyo-files.dooplex.hu/outpost.goauthentik.io/start?rd=$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: "Set-Cookie,X-Authentik-Username,X-Authentik-Email,X-Authentik-Name,X-Authentik-Groups,X-Authentik-Uid"
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Host $http_host;
nginx.ingress.kubernetes.io/configuration-snippet: |
set $geo_allowed 0;
if ($remote_addr ~ "^192\.168\.") { set $geo_allowed 1; }
if ($remote_addr ~ "^10\.") { set $geo_allowed 1; }
if ($geoip2_country_code = "HU") { set $geo_allowed 1; }
if ($geo_allowed = 0) {
return 403 "Access restricted to Hungary";
}
spec:
ingressClassName: nginx-internal
rules:
- host: workout.dooplex.hu
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wger
port:
number: 80
- host: workout.home
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wger
port:
number: 80
tls:
- hosts:
- workout.dooplex.hu
secretName: wger-tls
---
# ============================================================================
# Ingress #2: API paths (/api/) - NO forward-auth, JWT token auth only
# Required so the wger Flutter mobile app can hit /api/v2/token for login.
# More-specific path match means /api/* hits this Ingress, not the / one.
# ============================================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wger-api
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-api
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/configuration-snippet: |
# Same geo-block as the web UI ingress
set $geo_allowed 0;
if ($remote_addr ~ "^192\.168\.") { set $geo_allowed 1; }
if ($remote_addr ~ "^10\.") { set $geo_allowed 1; }
if ($geoip2_country_code = "HU") { set $geo_allowed 1; }
if ($geo_allowed = 0) {
return 403 "Access restricted to Hungary";
}
spec:
ingressClassName: nginx-internal
rules:
- host: workout.dooplex.hu
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: wger
port:
number: 80
- host: workout.home
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: wger
port:
number: 80
tls:
- hosts:
- workout.dooplex.hu
secretName: wger-tls
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wger-media
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-media
recurring-job-group.longhorn.io/needbackup: enabled
recurring-job.longhorn.io/source: enabled
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wger-static
namespace: workout-system
labels:
app.kubernetes.io/instance: wger
app.kubernetes.io/name: wger-static
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: wger-nginx-config
namespace: workout-system
data:
nginx.conf: |
server {
listen 80;
server_name _;
client_max_body_size 4G;
# Official Wger Logic
root /var/www/html/; # This is just a dummy root, aliases do the work
location /static/ {
alias /home/wger/static/;
expires 30d;
access_log off;
}
location /media/ {
alias /home/wger/media/;
expires 30d;
access_log off;
}
# API path: strip any client-supplied auth headers before proxying.
# Mobile app + API clients authenticate via JWT (/api/v2/token), not
# proxy auth. This is a defense-in-depth measure so that even if traffic
# somehow reaches this sidecar without going through the forward-auth
# ingress, it cannot forge an AUTH_PROXY login via a spoofed header.
# Nginx treats "" as "do not forward this header."
location /api/ {
proxy_pass http://localhost:8000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Authentik-Username "";
proxy_set_header X-Authentik-Email "";
proxy_set_header X-Authentik-Name "";
proxy_set_header X-Authentik-Groups "";
proxy_set_header X-Authentik-Uid "";
}
# Everything else: pass through the auth headers set by the
# forward-auth ingress so wger's AUTH_PROXY middleware can log the
# user in. $http_x_authentik_username expands to empty if the header
# isn't present (e.g. Tailscale admin access bypassing the ingress).
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Authentik-Username $http_x_authentik_username;
proxy_set_header X-Authentik-Email $http_x_authentik_email;
proxy_set_header X-Authentik-Name $http_x_authentik_name;
proxy_set_header X-Authentik-Groups $http_x_authentik_groups;
proxy_set_header X-Authentik-Uid $http_x_authentik_uid;
}
}
---