# 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; } } ---