Skip to content

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.


FilePurpose
argocd/app-configs/tandoor/namespace.yamlCreates tandoor namespace
argocd/app-configs/tandoor/middleware.yamlTraefik buffering middleware for uploads
argocd/app-configs/tandoor/secrets.yamlExternalSecrets: DB URL + app config
argocd/app-configs/tandoor/deployment.yamlDeployment + PVC (Gunicorn, port 8080)
argocd/app-configs/tandoor/service.yamlClusterIP service on port 8080
argocd/app-configs/tandoor/certificate.yamlcert-manager Certificate for TLS
argocd/app-configs/tandoor/ingress.yamlTraefik IngressRoute with middleware ref
argocd/app-configs/tandoor/kustomization.yamlKustomize resource list
argocd/app-configs/cnpg/db-tandoor.yamlCNPG Database CR
argocd/app-configs/cnpg/users-tandoor.yamlCNPG user ExternalSecret
argocd/cluster-app/templates/tandoor.yamlArgoCD Application
docs/reference/services.mdServices catalog entry

Files:

  • Create: argocd/app-configs/tandoor/namespace.yaml

  • Step 1: Write namespace manifest

---
apiVersion: v1
kind: Namespace
metadata:
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
Terminal window
jj describe -m "feat(tandoor): add namespace"

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/v1
kind: Database
metadata:
name: tandoor
namespace: postgres
spec:
cluster:
name: main
ensure: present
name: tandoor
owner: tandoor
  • Step 2: Write user ExternalSecret
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: main-tandoor-credentials
namespace: postgres
spec:
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
Terminal window
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/v1alpha1
kind: Middleware
metadata:
name: upload-buffer
namespace: tandoor
spec:
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
Terminal window
jj describe -m "feat(tandoor): add upload buffering middleware"

Files:

  • Create: argocd/app-configs/tandoor/secrets.yaml

  • Step 1: Write ExternalSecrets manifest

---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: tandoor-db-secrets
namespace: tandoor
spec:
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/v1
kind: ExternalSecret
metadata:
name: tandoor-app-secrets
namespace: tandoor
spec:
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
Terminal window
jj describe -m "feat(tandoor): add ExternalSecrets for db and app config"

Files:

  • Create: argocd/app-configs/tandoor/deployment.yaml

  • Step 1: Write deployment + PVC manifest

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: tandoor-data
namespace: tandoor
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tandoor
namespace: tandoor
labels:
app.kubernetes.io/name: tandoor
spec:
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: Always

Probe delays: livenessInitialDelay=45s and readinessInitialDelay=30s — allows boot.sh time to run collectstatic (~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
Terminal window
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: v1
kind: Service
metadata:
name: tandoor
namespace: tandoor
spec:
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/v1
kind: Certificate
metadata:
name: tandoor-tls
namespace: tandoor
spec:
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/v1alpha1
kind: IngressRoute
metadata:
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:

Terminal window
kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/service.yaml
kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/certificate.yaml
kubectl --context fzymgc-house apply --dry-run=client -f argocd/app-configs/tandoor/ingress.yaml

Expected: All three “created (dry run)”

  • Step 5: Commit
Terminal window
jj describe -m "feat(tandoor): add service, certificate, and ingress"

Files:

  • Create: argocd/app-configs/tandoor/kustomization.yaml

  • Step 1: Write kustomization manifest

---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: 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
Terminal window
jj describe -m "feat(tandoor): add kustomization"

Files:

  • Create: argocd/cluster-app/templates/tandoor.yaml

  • Step 1: Write ArgoCD Application manifest

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
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
Terminal window
jj describe -m "feat(argocd): add tandoor application"

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
Terminal window
jj describe -m "docs(services): add tandoor to services catalog"

This task requires Vault CLI access and cannot be automated. Run these commands from a shell with VAULT_ADDR and VAULT_TOKEN set.

  • Step 1: Generate Django SECRET_KEY
Terminal window
python3 -c "import secrets; print(secrets.token_urlsafe(50))"

Save the output for the next step.

  • Step 2: Create Vault secret for Tandoor
Terminal window
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 TBD values for client_id and secret initially — update after Authentik provider is created in Task 11.

  • Step 3: Create CNPG user secret
Terminal window
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
Terminal window
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"}}]}}'

  • Step 1: Review all files exist
Terminal window
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
Terminal window
jj bookmark create tandoor-recipes -r @
jj git push -b tandoor-recipes
gh pr create --title "feat(tandoor): deploy Tandoor Recipes alongside Mealie" \
--body "$(cat <<'EOF'
## Summary
Deploy 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
)"

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_ORIGINS env var