Tandoor Recipes Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Deploy Tandoor Recipes (vabene1111/recipes:1.6.0) in the tandoor namespace alongside Mealie for a feature/UX/performance bake-off comparison.
Architecture: Single-container Gunicorn deployment with CNPG PostgreSQL on the main cluster, Traefik ingress with buffering middleware, cert-manager TLS, Vault-backed ExternalSecrets, and OIDC auth via Authentik. Follows the identical isolation pattern as Mealie (namespace, PVC, secrets, ingress).
Tech Stack: Tandoor Recipes (Django/Gunicorn), CNPG PostgreSQL 18.1, Traefik v3, cert-manager, External Secrets Operator, Authentik OIDC, ArgoCD GitOps.
File Map
Section titled “File Map”| File | Purpose |
|---|---|
argocd/app-configs/tandoor/namespace.yaml | Creates tandoor namespace |
argocd/app-configs/tandoor/middleware.yaml | Traefik buffering middleware for uploads |
argocd/app-configs/tandoor/secrets.yaml | ExternalSecrets: DB URL + app config |
argocd/app-configs/tandoor/deployment.yaml | Deployment + PVC (Gunicorn, port 8080) |
argocd/app-configs/tandoor/service.yaml | ClusterIP service on port 8080 |
argocd/app-configs/tandoor/certificate.yaml | cert-manager Certificate for TLS |
argocd/app-configs/tandoor/ingress.yaml | Traefik IngressRoute with middleware ref |
argocd/app-configs/tandoor/kustomization.yaml | Kustomize resource list |
argocd/app-configs/cnpg/db-tandoor.yaml | CNPG Database CR |
argocd/app-configs/cnpg/users-tandoor.yaml | CNPG user ExternalSecret |
argocd/cluster-app/templates/tandoor.yaml | ArgoCD Application |
docs/reference/services.md | Services catalog entry |
Task 1: Create tandoor namespace
Section titled “Task 1: Create tandoor namespace”Files:
-
Create:
argocd/app-configs/tandoor/namespace.yaml -
Step 1: Write namespace manifest
---apiVersion: v1kind: Namespacemetadata: name: tandoor- Step 2: Validate with dry-run
Run: kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/namespace.yaml
Expected: “namespace/tandoor created (dry run)”
- Step 3: Commit
jj describe -m "feat(tandoor): add namespace"Task 2: Create CNPG database and user
Section titled “Task 2: Create CNPG database and user”Files:
- Create:
argocd/app-configs/cnpg/db-tandoor.yaml - Create:
argocd/app-configs/cnpg/users-tandoor.yaml - Step 1: Write Database CR
---apiVersion: postgresql.cnpg.io/v1kind: Databasemetadata: name: tandoor namespace: postgresspec: cluster: name: main ensure: present name: tandoor owner: tandoor- Step 2: Write user ExternalSecret
---apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: main-tandoor-credentials namespace: postgresspec: refreshPolicy: Periodic refreshInterval: 5m secretStoreRef: name: vault kind: ClusterSecretStore target: name: main-tandoor-credentials creationPolicy: Owner deletionPolicy: Delete template: metadata: labels: cnpg.io/reload: "true" type: kubernetes.io/basic-auth data: username: "{{ .username }}" password: "{{ .password }}" data: - secretKey: username remoteRef: key: fzymgc-house/cluster/postgres/users/main-tandoor property: username - secretKey: password remoteRef: key: fzymgc-house/cluster/postgres/users/main-tandoor property: password- Step 3: Validate
Run: kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/cnpg/db-tandoor.yaml && kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/cnpg/users-tandoor.yaml
Expected: Both “created (dry run)”
- Step 4: Commit
jj describe -m "feat(cnpg): add tandoor database and user"Task 3: Create Traefik buffering middleware
Section titled “Task 3: Create Traefik buffering middleware”Files:
-
Create:
argocd/app-configs/tandoor/middleware.yaml -
Step 1: Write middleware manifest
---apiVersion: traefik.io/v1alpha1kind: Middlewaremetadata: name: upload-buffer namespace: tandoorspec: buffering: maxRequestBodyBytes: 268435456 memRequestBodyBytes: 10485760- Step 2: Validate
Run: kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/middleware.yaml
Expected: “middleware.traefik.io/upload-buffer created (dry run)”
- Step 3: Commit
jj describe -m "feat(tandoor): add upload buffering middleware"Task 4: Create ExternalSecrets
Section titled “Task 4: Create ExternalSecrets”Files:
-
Create:
argocd/app-configs/tandoor/secrets.yaml -
Step 1: Write ExternalSecrets manifest
---apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: tandoor-db-secrets namespace: tandoorspec: refreshPolicy: Periodic refreshInterval: 5m secretStoreRef: name: vault kind: ClusterSecretStore target: name: tandoor-db-secrets creationPolicy: Owner deletionPolicy: Delete template: type: Opaque data: db-url: "postgresql://{{ .postgres_user }}:{{ .postgres_password }}@main-rw.postgres.svc.cluster.local:5432/tandoor" data: - secretKey: postgres_user remoteRef: key: fzymgc-house/cluster/postgres/users/main-tandoor property: username - secretKey: postgres_password remoteRef: key: fzymgc-house/cluster/postgres/users/main-tandoor property: password---apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: tandoor-app-secrets namespace: tandoorspec: refreshPolicy: Periodic refreshInterval: 5m secretStoreRef: name: vault kind: ClusterSecretStore target: name: tandoor-app-secrets creationPolicy: Owner deletionPolicy: Delete template: type: Opaque data: base-url: "{{ .base_url }}" secret-key: "{{ .secret_key }}" enable-signup: "{{ .enable_signup }}" gunicorn-media: "{{ .gunicorn_media }}" socialaccount-providers: "{{ .socialaccount_providers }}" data: - secretKey: base_url remoteRef: key: fzymgc-house/cluster/tandoor property: base_url - secretKey: secret_key remoteRef: key: fzymgc-house/cluster/tandoor property: secret_key - secretKey: enable_signup remoteRef: key: fzymgc-house/cluster/tandoor property: enable_signup - secretKey: gunicorn_media remoteRef: key: fzymgc-house/cluster/tandoor property: gunicorn_media - secretKey: socialaccount_providers remoteRef: key: fzymgc-house/cluster/tandoor property: socialaccount_providers- Step 2: Validate
Run: kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/secrets.yaml
Expected: Both ExternalSecrets “created (dry run)”
- Step 3: Commit
jj describe -m "feat(tandoor): add ExternalSecrets for db and app config"Task 5: Create deployment and PVC
Section titled “Task 5: Create deployment and PVC”Files:
-
Create:
argocd/app-configs/tandoor/deployment.yaml -
Step 1: Write deployment + PVC manifest
---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: tandoor-data namespace: tandoorspec: accessModes: - ReadWriteOnce storageClassName: longhorn resources: requests: storage: 10Gi---apiVersion: apps/v1kind: Deploymentmetadata: name: tandoor namespace: tandoor labels: app.kubernetes.io/name: tandoorspec: replicas: 1 strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: tandoor template: metadata: labels: app.kubernetes.io/name: tandoor annotations: reloader.stakater.com/auto: "true" spec: containers: - name: tandoor image: vabene1111/recipes:1.6.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http protocol: TCP env: - name: DATABASE_URL valueFrom: secretKeyRef: name: tandoor-db-secrets key: db-url - name: DB_ENGINE value: "django.db.backends.postgresql" - name: ALLOWED_HOSTS value: "recipes.fzymgc.house,recipes.k8s.fzymgc.house" - name: TZ value: "America/Los_Angeles" - name: DEBUG value: "0" - name: ENABLE_SIGNUP valueFrom: secretKeyRef: name: tandoor-app-secrets key: enable-signup - name: GUNICORN_MEDIA valueFrom: secretKeyRef: name: tandoor-app-secrets key: gunicorn-media - name: SECRET_KEY valueFrom: secretKeyRef: name: tandoor-app-secrets key: secret-key - name: SOCIAL_PROVIDERS value: "allauth.socialaccount.providers.openid_connect" - name: SOCIALACCOUNT_PROVIDERS valueFrom: secretKeyRef: name: tandoor-app-secrets key: socialaccount-providers - name: SOCIAL_DEFAULT_ACCESS value: "1" - name: SOCIAL_DEFAULT_GROUP value: "1" volumeMounts: - name: tandoor-data mountPath: /opt/recipes/mediafiles resources: requests: memory: "1Gi" cpu: "1000m" limits: memory: "2Gi" cpu: "2000m" livenessProbe: httpGet: path: /system/ port: http initialDelaySeconds: 45 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /system/ port: http initialDelaySeconds: 30 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 volumes: - name: tandoor-data persistentVolumeClaim: claimName: tandoor-data restartPolicy: AlwaysProbe delays:
livenessInitialDelay=45sandreadinessInitialDelay=30s— allowsboot.shtime to runcollectstatic(~5-10s) + DB migration (~5-15s) + Gunicorn startup before probes fire. Longer than Mealie’s delays because Tandoor has more startup work.
- Step 2: Validate
Run: kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/deployment.yaml
Expected: PVC and Deployment “created (dry run)”
- Step 3: Commit
jj describe -m "feat(tandoor): add deployment and PVC"Task 6: Create service, certificate, and ingress
Section titled “Task 6: Create service, certificate, and ingress”Files:
- Create:
argocd/app-configs/tandoor/service.yaml - Create:
argocd/app-configs/tandoor/certificate.yaml - Create:
argocd/app-configs/tandoor/ingress.yaml - Step 1: Write service manifest
---apiVersion: v1kind: Servicemetadata: name: tandoor namespace: tandoorspec: type: ClusterIP selector: app.kubernetes.io/name: tandoor ports: - protocol: TCP port: 8080 targetPort: http name: http- Step 2: Write certificate manifest
---apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: tandoor-tls namespace: tandoorspec: secretName: tandoor-tls issuerRef: name: vault-issuer kind: ClusterIssuer dnsNames: - recipes.fzymgc.house - recipes.k8s.fzymgc.house - tandoor.tandoor.svc.cluster.local - tandoor.tandoor.svc - tandoor usages: - server auth- Step 3: Write ingress manifest
---apiVersion: traefik.io/v1alpha1kind: IngressRoutemetadata: name: tandoor namespace: tandoor annotations: router-hosts.fzymgc.house/enabled: "true"spec: entryPoints: - websecure routes: - match: Host(`recipes.fzymgc.house`) kind: Rule services: - name: tandoor port: 8080 middlewares: - name: upload-buffer - match: Host(`recipes.k8s.fzymgc.house`) kind: Rule services: - name: tandoor port: 8080 middlewares: - name: upload-buffer tls: secretName: tandoor-tls- Step 4: Validate all three
Run:
kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/service.yamlkubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/certificate.yamlkubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/ingress.yamlExpected: All three “created (dry run)”
- Step 5: Commit
jj describe -m "feat(tandoor): add service, certificate, and ingress"Task 7: Create kustomization.yaml
Section titled “Task 7: Create kustomization.yaml”Files:
-
Create:
argocd/app-configs/tandoor/kustomization.yaml -
Step 1: Write kustomization manifest
---apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomization
namespace: tandoor
resources: - namespace.yaml - middleware.yaml - secrets.yaml - deployment.yaml - service.yaml - certificate.yaml - ingress.yaml- Step 2: Validate kustomize output
Run: kubectl --context fzymgc-house kustomize argocd/app-configs/tandoor | head -20
Expected: Rendered YAML output with all resources
- Step 3: Commit
jj describe -m "feat(tandoor): add kustomization"Task 8: Create ArgoCD Application
Section titled “Task 8: Create ArgoCD Application”Files:
-
Create:
argocd/cluster-app/templates/tandoor.yaml -
Step 1: Write ArgoCD Application manifest
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: tandoor namespace: argocd annotations: argocd.argoproj.io/sync-wave: "1"spec: project: core-services sources: - repoURL: https://github.com/fzymgc-house/selfhosted-cluster targetRevision: HEAD path: argocd/app-configs/tandoor destination: server: https://kubernetes.default.svc namespace: tandoor syncPolicy: automated: prune: true selfHeal: true syncOptions: - ServerSideApply=true - CreateNamespace=true - RespectIgnoreDifferences=true ignoreDifferences: - kind: ExternalSecret group: external-secrets.io jqPathExpressions: - .spec.data[].remoteRef.conversionStrategy - .spec.data[].remoteRef.decodingStrategy - .spec.data[].remoteRef.metadataPolicy- Step 2: Validate
Run: kubectl --context fzymgc-house apply --dry-run=client -f argocd/cluster-app/templates/tandoor.yaml
Expected: “application.argoproj.io/tandoor created (dry run)”
- Step 3: Commit
jj describe -m "feat(argocd): add tandoor application"Task 9: Update services catalog
Section titled “Task 9: Update services catalog”Files:
-
Modify:
docs/reference/services.md -
Step 1: Add Tandoor entry to services table
Insert after Mealie’s row in the Quick Reference table:
| [Tandoor](#tandoor) | `recipes.fzymgc.house` | tandoor | Application |- Step 2: Add Tandoor section after Mealie
Insert after the Mealie section under Application Services:
### Tandoor
| Property | Value ||----------|-------|| URL | `recipes.fzymgc.house` || Alt URL | `recipes.k8s.fzymgc.house` || Namespace | `tandoor` || Ingress Type | Traefik IngressRoute || Auth Method | OIDC (Authentik via Django Allauth) || Vault Path | `secret/fzymgc-house/cluster/tandoor` || Status | In Progress |- Step 3: Commit
jj describe -m "docs(services): add tandoor to services catalog"Task 10: Vault secrets (manual)
Section titled “Task 10: Vault secrets (manual)”This task requires Vault CLI access and cannot be automated. Run these commands from a shell with
VAULT_ADDRandVAULT_TOKENset.
- Step 1: Generate Django SECRET_KEY
python3 -c "import secrets; print(secrets.token_urlsafe(50))"Save the output for the next step.
- Step 2: Create Vault secret for Tandoor
vault kv put secret/fzymgc-house/cluster/tandoor \ base_url="https://recipes.fzymgc.house" \ secret_key="<output-from-step-1>" \ enable_signup="1" \ gunicorn_media="1" \ socialaccount_providers='{"openid_connect":{"OAUTH_PKCE_ENABLED":true,"APPS":[{"provider_id":"authentik","name":"Authentik","client_id":"TBD","secret":"TBD","settings":{"server_url":"https://auth.fzymgc.house/application/o/tandoor/.well-known/openid-configuration"}}]}}'Note: Use placeholder
TBDvalues forclient_idandsecretinitially — update after Authentik provider is created in Task 11.
- Step 3: Create CNPG user secret
vault kv put secret/fzymgc-house/cluster/postgres/users/main-tandoor \ username="tandoor" \ password="$(python3 -c 'import secrets,string; print("".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(32)))')"Task 11: Authentik OIDC configuration (manual)
Section titled “Task 11: Authentik OIDC configuration (manual)”This task requires Authentik admin UI access. Navigate to
https://auth.fzymgc.house/.
- Step 1: Create OIDC Provider
Under Applications → Providers → Create:
- Type: OIDC Provider
- Name: Tandoor
- Client ID:
tandoor - Client Secret: Generate and copy
- Redirect URI:
https://recipes.fzymgc.house/accounts/oidc/authentik/login/callback/ - Signing Key: Authentik default
- Scopes:
openid email profile - Subject mode: Based on user’s email
- Step 2: Create Application
Under Applications → Applications → Create:
- Name: Tandoor
- Slug: tandoor
- Provider: Select “Tandoor” (created in Step 1)
- UI Settings: (optional — set icon if desired)
- Step 3: Assign to outpost
Edit the outpost (typically authentik-outpost) and ensure the new Tandoor application is included.
- Step 4: Update Vault with real credentials
vault kv patch secret/fzymgc-house/cluster/tandoor \ socialaccount_providers='{"openid_connect":{"OAUTH_PKCE_ENABLED":true,"APPS":[{"provider_id":"authentik","name":"Authentik","client_id":"tandoor","secret":"<actual-client-secret>","settings":{"server_url":"https://auth.fzymgc.house/application/o/tandoor/.well-known/openid-configuration"}}]}}'Task 12: Final validation and sync
Section titled “Task 12: Final validation and sync”- Step 1: Review all files exist
ls -la argocd/app-configs/tandoor/Expected: namespace.yaml, middleware.yaml, secrets.yaml, deployment.yaml, service.yaml, certificate.yaml, ingress.yaml, kustomization.yaml
- Step 2: Validate full kustomize build
Run: kubectl --context fzymgc-house kustomize argocd/app-configs/tandoor | kubectl --context fzymgc-house apply --dry-run=client -f -
Expected: All resources “created (dry run)” with no errors
- Step 3: Verify no YAML lint issues
Run: yamllint argocd/app-configs/tandoor/
Expected: Clean output, no errors
- Step 4: Push all and create PR
jj bookmark create tandoor-recipes -r @jj git push -b tandoor-recipesgh pr create --title "feat(tandoor): deploy Tandoor Recipes alongside Mealie" \ --body "$(cat <<'EOF'## SummaryDeploy Tandoor Recipes (v1.6.0) alongside Mealie for a feature/UX/performance bake-off comparison.
## Architecture- Namespace: `tandoor`- CNPG database on `main` cluster- Gunicorn serving app + static + media (no nginx)- OIDC via Authentik (Django Allauth)- Traefik IngressRoute with buffering middleware for uploads- Vault-backed ExternalSecrets
## New files- `argocd/app-configs/tandoor/` — 8 manifests (ns, mw, secrets, deploy, svc, cert, ingress, kustomize)- `argocd/app-configs/cnpg/db-tandoor.yaml` — CNPG Database CR- `argocd/app-configs/cnpg/users-tandoor.yaml` — CNPG user ExternalSecret- `argocd/cluster-app/templates/tandoor.yaml` — ArgoCD Application
## Prerequisites- [ ] Vault secrets created (`secret/fzymgc-house/cluster/tandoor`, `.../postgres/users/main-tandoor`)- [ ] Authentik OIDC provider + application configured- [ ] DNS for `recipes.fzymgc.house` and `recipes.k8s.fzymgc.house`
## Post-deploy verification- [ ] Pod starts and passes health checks (`/system/` responds 200)- [ ] OIDC login via Authentik works (accounts/oidc/authentik/login/callback/)- [ ] Recipe creation and image upload work- [ ] Gunicorn media warning on /system/ page is acknowledged (GUNICORN_MEDIA=1)
🤖 Generated with [Claude Code](https://claude.com/claude-code)EOF)"Deployment Watch Checklist
Section titled “Deployment Watch Checklist”After ArgoCD syncs:
- Pod reaches
1/1 Ready -
/system/returns 200 -
kubectl logs -n tandoor deployment/tandoor | grep -i error— no unexpected errors - OIDC login flow: visit
https://recipes.fzymgc.house, click SSO button, authenticate, verify redirect back - Upload a test recipe with an image — verify no 413/403 errors
- CSRF: create a recipe via POST — if 403, add
CSRF_TRUSTED_ORIGINSenv var