Deploy Mealie recipe manager (#299)
All checks were successful
Build Container (Nix) / detect (push) Successful in 2s
Build Container / detect (push) Successful in 2s
Build Container (Nix) / build (mealie) (push) Successful in 2s
Build Container / build (mealie) (push) Successful in 8s

## Summary

- Deploy Mealie (self-hosted recipe manager) on minikube-indri via ArgoCD
- Build container from source via forge mirror (`mirrors/mealie`) — multi-stage Dockerfile with Node.js frontend + Python/uv backend
- Add Caddy proxy entry for `meals.ops.eblu.me`
- Part of a larger meal planning pipeline: Mealie stores categorized recipes, a planner script selects balanced meals, and Ollama generates unified cooking timelines

## Status

- [x] Mirror mealie repo on forge
- [x] Dockerfile (from-source build)
- [x] ArgoCD app + k8s manifests
- [x] Caddy proxy entry
- [x] Service docs, routing table, app registry
- [ ] Local Dagger build test
- [ ] Container build + push to registry
- [ ] Update kustomization.yaml with real image tag
- [ ] Deploy and verify
- [ ] Provision Caddy

## Test plan

- Build container locally via `dagger call build --src=. --container-name=mealie`
- Trigger CI build via `mise run container-build-and-release mealie`
- Deploy from branch: `argocd app set mealie --revision deploy-mealie && argocd app sync mealie`
- Verify Mealie UI at `https://meals.ops.eblu.me`
- Verify API docs at `https://meals.ops.eblu.me/docs`

Reviewed-on: #299
This commit is contained in:
Erich Blume 2026-03-16 21:59:10 -07:00
commit 11330ebea0
22 changed files with 489 additions and 3 deletions

17
argocd/apps/mealie.yaml Normal file
View file

@ -0,0 +1,17 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: mealie
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/mealie
destination:
server: https://kubernetes.default.svc
namespace: mealie
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -345,3 +345,47 @@ data:
provider: !KeyOf jellyfin-provider
meta_launch_url: https://jellyfin.ops.eblu.me
policy_engine_mode: all
mealie.yaml: |
version: 1
metadata:
name: BlumeOps Mealie SSO
labels:
blueprints.goauthentik.io/description: "Mealie OIDC provider and application"
entries:
# OAuth2 provider for Mealie (confidential — Mealie requires client_secret)
- model: authentik_providers_oauth2.oauth2provider
id: mealie-provider
identifiers:
name: Mealie
attrs:
name: Mealie
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
client_type: confidential
client_id: mealie
client_secret: !Env AUTHENTIK_MEALIE_CLIENT_SECRET
redirect_uris:
- matching_mode: strict
url: https://meals.ops.eblu.me/login
- matching_mode: strict
url: https://meals.tail8d86e.ts.net/login
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
property_mappings:
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
sub_mode: hashed_user_id
include_claims_in_id_token: true
# Mealie application — all authenticated users allowed (admin mapped via OIDC_ADMIN_GROUP)
- model: authentik_core.application
id: mealie-app
identifiers:
slug: mealie
attrs:
name: Mealie
slug: mealie
provider: !KeyOf mealie-provider
meta_launch_url: https://meals.ops.eblu.me
policy_engine_mode: all

View file

@ -78,6 +78,11 @@ spec:
secretKeyRef:
name: authentik-config
key: argocd-client-secret
- name: AUTHENTIK_MEALIE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: authentik-config
key: mealie-client-secret
volumeMounts:
- name: blueprints
mountPath: /blueprints/custom

View file

@ -57,3 +57,7 @@ spec:
remoteRef:
key: "Authentik (blumeops)"
property: argocd-client-secret
- secretKey: mealie-client-secret
remoteRef:
key: "Authentik (blumeops)"
property: mealie-client-secret

View file

@ -0,0 +1,79 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mealie
namespace: mealie
spec:
replicas: 1
selector:
matchLabels:
app: mealie
template:
metadata:
labels:
app: mealie
spec:
containers:
- name: mealie
image: registry.ops.eblu.me/blumeops/mealie:kustomized
ports:
- containerPort: 9000
env:
- name: BASE_URL
value: "https://meals.ops.eblu.me"
- name: ALLOW_SIGNUP
value: "false"
- name: TZ
value: "America/Los_Angeles"
- name: MAX_WORKERS
value: "1"
- name: WEB_CONCURRENCY
value: "1"
# OIDC — Authentik (public client, PKCE)
- name: OIDC_AUTH_ENABLED
value: "true"
- name: OIDC_CONFIGURATION_URL
value: "https://authentik.ops.eblu.me/application/o/mealie/.well-known/openid-configuration"
- name: OIDC_CLIENT_ID
value: "mealie"
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: mealie-oidc
key: client-secret
- name: OIDC_AUTO_REDIRECT
value: "false"
- name: OIDC_PROVIDER_NAME
value: "Authentik"
- name: OIDC_ADMIN_GROUP
value: "admins"
- name: OIDC_SIGNUP_ENABLED
value: "true"
- name: OIDC_USER_CLAIM
value: "email"
volumeMounts:
- name: data
mountPath: /app/data
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "1000Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/app/about
port: 9000
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/app/about
port: 9000
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: data
persistentVolumeClaim:
claimName: mealie-data

View file

@ -0,0 +1,19 @@
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mealie-oidc
namespace: mealie
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-blumeops
target:
name: mealie-oidc
creationPolicy: Owner
data:
- secretKey: client-secret
remoteRef:
key: "Authentik (blumeops)"
property: mealie-client-secret

View file

@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mealie-tailscale
namespace: mealie
annotations:
tailscale.com/proxy-class: "default"
tailscale.com/proxy-group: "ingress"
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "Mealie"
gethomepage.dev/group: "Home"
gethomepage.dev/icon: "mealie.png"
gethomepage.dev/description: "Recipe manager"
gethomepage.dev/href: "https://meals.ops.eblu.me"
gethomepage.dev/pod-selector: "app=mealie"
spec:
ingressClassName: tailscale
defaultBackend:
service:
name: mealie
port:
number: 9000
tls:
- hosts:
- meals

View file

@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: mealie
resources:
- deployment.yaml
- service.yaml
- pvc.yaml
- ingress-tailscale.yaml
- external-secret.yaml
images:
- name: registry.ops.eblu.me/blumeops/mealie
newTag: v3.12.0-5c5fd18

View file

@ -0,0 +1,13 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mealie-data
namespace: mealie
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 2Gi

View file

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: mealie
namespace: mealie
spec:
selector:
app: mealie
ports:
- name: http
port: 9000
targetPort: 9000
protocol: TCP