From bd2b02f51c6c503efcb06322851ac2438af88bbb Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 21:07:25 -0700 Subject: [PATCH] Add Mealie recipe manager service Deploy Mealie on minikube-indri for meal planning and prep automation. Built from source via forge mirror (mirrors/mealie) with multi-stage Dockerfile: Node.js frontend + Python/uv backend. Includes ArgoCD app, k8s manifests, Caddy proxy entry, and service documentation. Co-Authored-By: Claude Opus 4.6 (1M context) --- ansible/roles/caddy/defaults/main.yml | 3 + argocd/apps/mealie.yaml | 17 +++ argocd/manifests/mealie/deployment.yaml | 57 +++++++ .../manifests/mealie/ingress-tailscale.yaml | 25 +++ argocd/manifests/mealie/kustomization.yaml | 14 ++ argocd/manifests/mealie/pvc.yaml | 13 ++ argocd/manifests/mealie/service.yaml | 13 ++ containers/mealie/Dockerfile | 142 ++++++++++++++++++ docs/changelog.d/deploy-mealie.feature.md | 1 + docs/reference/infrastructure/routing.md | 1 + docs/reference/kubernetes/apps.md | 1 + docs/reference/services/mealie.md | 51 +++++++ 12 files changed, 338 insertions(+) create mode 100644 argocd/apps/mealie.yaml create mode 100644 argocd/manifests/mealie/deployment.yaml create mode 100644 argocd/manifests/mealie/ingress-tailscale.yaml create mode 100644 argocd/manifests/mealie/kustomization.yaml create mode 100644 argocd/manifests/mealie/pvc.yaml create mode 100644 argocd/manifests/mealie/service.yaml create mode 100644 containers/mealie/Dockerfile create mode 100644 docs/changelog.d/deploy-mealie.feature.md create mode 100644 docs/reference/services/mealie.md diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index a9576a1..dbf0b13 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -91,6 +91,9 @@ caddy_services: - name: ollama host: "ollama.{{ caddy_domain }}" backend: "https://ollama.tail8d86e.ts.net" + - name: mealie + host: "meals.{{ caddy_domain }}" + backend: "https://meals.tail8d86e.ts.net" - name: sifaka host: "nas.{{ caddy_domain }}" backend: "http://sifaka:5000" diff --git a/argocd/apps/mealie.yaml b/argocd/apps/mealie.yaml new file mode 100644 index 0000000..af33469 --- /dev/null +++ b/argocd/apps/mealie.yaml @@ -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 diff --git a/argocd/manifests/mealie/deployment.yaml b/argocd/manifests/mealie/deployment.yaml new file mode 100644 index 0000000..1c99ed0 --- /dev/null +++ b/argocd/manifests/mealie/deployment.yaml @@ -0,0 +1,57 @@ +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" + 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 diff --git a/argocd/manifests/mealie/ingress-tailscale.yaml b/argocd/manifests/mealie/ingress-tailscale.yaml new file mode 100644 index 0000000..a885e15 --- /dev/null +++ b/argocd/manifests/mealie/ingress-tailscale.yaml @@ -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 diff --git a/argocd/manifests/mealie/kustomization.yaml b/argocd/manifests/mealie/kustomization.yaml new file mode 100644 index 0000000..44bc386 --- /dev/null +++ b/argocd/manifests/mealie/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: mealie + +resources: + - deployment.yaml + - service.yaml + - pvc.yaml + - ingress-tailscale.yaml + +images: + - name: registry.ops.eblu.me/blumeops/mealie + newTag: v3.12.0-0000000 diff --git a/argocd/manifests/mealie/pvc.yaml b/argocd/manifests/mealie/pvc.yaml new file mode 100644 index 0000000..e3b4e63 --- /dev/null +++ b/argocd/manifests/mealie/pvc.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mealie-data + namespace: mealie +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 2Gi diff --git a/argocd/manifests/mealie/service.yaml b/argocd/manifests/mealie/service.yaml new file mode 100644 index 0000000..4162b96 --- /dev/null +++ b/argocd/manifests/mealie/service.yaml @@ -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 diff --git a/containers/mealie/Dockerfile b/containers/mealie/Dockerfile new file mode 100644 index 0000000..fe1bf02 --- /dev/null +++ b/containers/mealie/Dockerfile @@ -0,0 +1,142 @@ +# Mealie — self-hosted recipe manager +# Built from source via forge mirror of mealie-recipes/mealie +# Based on upstream docker/Dockerfile (multi-stage: Node frontend + Python backend) + +ARG CONTAINER_APP_VERSION=v3.12.0 + +############################################### +# Frontend Build +############################################### +FROM node:24-slim AS frontend-builder + +ARG CONTAINER_APP_VERSION +RUN apt-get update && apt-get install --no-install-recommends -y git ca-certificates && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \ + https://forge.ops.eblu.me/mirrors/mealie.git /src + +WORKDIR /src/frontend + +RUN yarn install \ + --prefer-offline \ + --frozen-lockfile \ + --non-interactive \ + --production=false \ + --network-timeout 1000000 + +RUN yarn generate + +############################################### +# Python Base +############################################### +FROM python:3.12-slim AS python-base + +ENV MEALIE_HOME="/app" +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + VENV_PATH="/opt/mealie" + +ENV PATH="$VENV_PATH/bin:$PATH" + +RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \ + && usermod -G users abc \ + && mkdir $MEALIE_HOME + +############################################### +# Backend Package Build +############################################### +FROM python-base AS backend-builder + +ARG CONTAINER_APP_VERSION +RUN apt-get update \ + && apt-get install --no-install-recommends -y curl git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install uv + +RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \ + https://forge.ops.eblu.me/mirrors/mealie.git /src + +WORKDIR /src + +COPY --from=frontend-builder /src/frontend/dist ./mealie/frontend + +RUN uv build --out-dir dist + +RUN uv export --no-editable --no-emit-project --extra pgsql --format requirements-txt --output-file dist/requirements.txt \ + && MEALIE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") \ + && echo "mealie[pgsql]==${MEALIE_VERSION} \\" >> dist/requirements.txt \ + && pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt \ + && echo " \\" >> dist/requirements.txt \ + && pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt + +############################################### +# Python Venv Build +############################################### +FROM python-base AS venv-builder + +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + build-essential \ + libpq-dev \ + libwebp-dev \ + ffmpeg \ + libsasl2-dev libldap2-dev libssl-dev \ + gnupg gnupg2 gnupg1 \ + && rm -rf /var/lib/apt/lists/* + +RUN python3 -m venv --upgrade-deps $VENV_PATH + +COPY --from=backend-builder /src/dist /dist + +RUN . $VENV_PATH/bin/activate \ + && pip install --require-hashes -r /dist/requirements.txt --find-links /dist + +############################################### +# Production Image +############################################### +FROM python-base AS production + +ENV PRODUCTION=true +ENV TESTING=false + +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + curl \ + ffmpeg \ + gosu \ + iproute2 \ + libldap-common \ + libldap2 \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /run/secrets + +COPY --from=venv-builder $VENV_PATH $VENV_PATH + +ENV NLTK_DATA="/nltk_data/" +RUN mkdir -p $NLTK_DATA +RUN python -m nltk.downloader -d $NLTK_DATA averaged_perceptron_tagger_eng + +VOLUME ["$MEALIE_HOME/data/"] +ENV APP_PORT=9000 + +EXPOSE ${APP_PORT} + +COPY --from=backend-builder /src/docker/healthcheck.sh $MEALIE_HOME/healthcheck.sh +RUN chmod +x $MEALIE_HOME/healthcheck.sh +HEALTHCHECK CMD $MEALIE_HOME/healthcheck.sh + +ENV HOST=0.0.0.0 + +COPY --from=backend-builder /src/docker/entry.sh $MEALIE_HOME/run.sh +RUN chmod +x $MEALIE_HOME/run.sh + +LABEL org.opencontainers.image.title="Mealie" +LABEL org.opencontainers.image.description="Self-hosted recipe manager" +LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie" + +ENTRYPOINT ["/app/run.sh"] diff --git a/docs/changelog.d/deploy-mealie.feature.md b/docs/changelog.d/deploy-mealie.feature.md new file mode 100644 index 0000000..5b85426 --- /dev/null +++ b/docs/changelog.d/deploy-mealie.feature.md @@ -0,0 +1 @@ +Deploy Mealie recipe manager on minikube-indri for meal planning and prep automation. diff --git a/docs/reference/infrastructure/routing.md b/docs/reference/infrastructure/routing.md index 91457e9..c85dbb5 100644 --- a/docs/reference/infrastructure/routing.md +++ b/docs/reference/infrastructure/routing.md @@ -40,6 +40,7 @@ DNS points to [[indri]]'s Tailscale IP. TLS via Let's Encrypt (ACME DNS-01 with | [[navidrome]] | https://dj.ops.eblu.me | Music streaming | | [[jellyfin]] | https://jellyfin.ops.eblu.me | Media server | | [[postgresql]] | pg.ops.eblu.me:5432 | Database | +| [[mealie]] | https://meals.ops.eblu.me | Recipe manager | | [[sifaka|Sifaka]] | https://nas.ops.eblu.me | NAS dashboard | ## Public Services (`*.eblu.me`) diff --git a/docs/reference/kubernetes/apps.md b/docs/reference/kubernetes/apps.md index f584d5a..270cc55 100644 --- a/docs/reference/kubernetes/apps.md +++ b/docs/reference/kubernetes/apps.md @@ -39,6 +39,7 @@ Registry of all applications deployed via [[argocd]]. | `cv` | cv | `argocd/manifests/cv/` | [[cv]] | | `forgejo-runner` | forgejo-runner | `argocd/manifests/forgejo-runner/` | [[forgejo]] CI | | `ollama` | ollama | `argocd/manifests/ollama/` | [[ollama]] | +| `mealie` | mealie | `argocd/manifests/mealie/` | [[mealie]] | ## Sync Policies diff --git a/docs/reference/services/mealie.md b/docs/reference/services/mealie.md new file mode 100644 index 0000000..3bdcd87 --- /dev/null +++ b/docs/reference/services/mealie.md @@ -0,0 +1,51 @@ +--- +title: Mealie +modified: 2026-03-16 +tags: + - service + - recipes +--- + +# Mealie + +Self-hosted recipe manager with a REST API. Part of the meal planning pipeline: Mealie stores categorized recipes, a planner script selects balanced meals, and [[ollama]] generates a unified cooking timeline. + +## Quick Reference + +| Property | Value | +|----------|-------| +| **URL** | https://meals.ops.eblu.me | +| **Tailscale URL** | https://meals.tail8d86e.ts.net | +| **Namespace** | `mealie` | +| **Image** | `registry.ops.eblu.me/blumeops/mealie` (built from source) | +| **Database** | SQLite (local, at `/app/data/`) | +| **API Docs** | https://meals.ops.eblu.me/docs | +| **Upstream** | https://github.com/mealie-recipes/mealie | +| **Manifests** | `argocd/manifests/mealie/` | + +## Features + +- Full REST API (FastAPI) for recipe CRUD, filtering by tag/category +- Structured recipe data: ingredients (quantity/unit/food), step-by-step instructions +- Built-in meal planning and shopping lists +- Recipe import from URLs +- API token auth for automation + +## Storage + +- 2Gi PVC at `/app/data/` via `local-path` storageClassName +- SQLite database (sufficient for single-user, no network storage concerns on minikube) +- Recipe images and assets stored alongside the database + +## Networking + +| Endpoint | Reachable from | +|----------|----------------| +| `https://meals.ops.eblu.me` | Tailnet clients (via Caddy) | +| `https://meals.tail8d86e.ts.net` | Tailnet clients | +| `http://mealie.mealie.svc.cluster.local:9000` | In-cluster | + +## Related + +- [[ollama]] — LLM backend for meal timeline generation +- [[borgmatic]] — Data backup