Mealie


Mealie Mealie is a self hosted recipe manager. It can easily import recipes from your favorite web sites and then generate meal plans and shopping lists. Below is how I've installed Mealie in my Kubernetes cluster.

Product: Mealie
Install Type: Manifest Files
Container Image: Github

Installation Details

Before beginning your installation, I'd recommend reading through the installation instructions to get an idea of what is needed for your particular installation. For example, do you want to use a PostgreSQL database or SQLIte? It may depend of where you plan on installing Mealie. If you are planning to use a NAS as storage, a PostgreSQL database probably makes more sense.

Because of my setup, and the fact I already have a PostgreSQL already running in my Kubernetes cluster, I opted to adapt the Docker Compose for Mealie for Postgres for my installation. However, you can simply not define the environment for Postgres to default use SQLite.

Now let's create the files we'll need to configure Mealie in Kubernetes

The following manifest files assume you will want to install this to a namespace named utility, an nginx ingress named nginx, and Cert Manager configured to use the ACME provider Letsencrypt. Please adjust for your particular needs.

00-utility-namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: utility
  labels:
    name: utility

01-config.yaml

This will define the environment variables for your Mealie installation. You should consult the Mealie Backend Configuration for configuration options. I configure some general settings, SMTP Email, and OIDC below, but there are additional settings like LDAP support that can also be configured. I am currently using Authentik for Single Sign On, but there are many viable alternatives to give you this functionality.

I have created a ConfigMap for most settings, but I also configure a Secret for more sensitive settings.

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mealie-configs
  namespace: utility
data:
  TZ: "America/New York"
  BASE_URL: "https://your.mealie.doman"
  ALLOW_SIGNUP: "false"
  TOKEN_TIME: "1"
  # Email Settings
  SMTP_HOST: "your.smtp.host"
  SMTP_PORT: "3325"
  SMTP_FROM_NAME: "Mealie"
  SMTP_AUTH_STRATEGY: "TLS"
  SMTP_FROM_EMAIL: "mealie-noreply@your.mail.domain"
  # Database Settings
  DB_ENGINE: "postgres"
  POSTGRES_SERVER: "your.postgresql.server"
  POSTGRES_PORT: "5432"
  POSTGRES_DB: "mealie"
  # OIDC Settings
  OIDC_AUTH_ENABLED: "true"
  OIDC_CONFIGURATION_URL: " Your config endpoint, usually ends in /.well-known/openid-configuration"
  OIDC_SIGNUP_ENABLED: "true"
  OIDC_USER_GROUP: "users"
  OIDC_ADMIN_GROUP: "admins"
  OIDC_AUTO_REDIRECT: "false"
  OIDC_PROVIDER_NAME: "SSO"
  OIDC_REMEMBER_ME: "false"
  OIDC_SIGNING_ALGORITHM: "RS256"
  OIDC_USER_CLAIM: "email"
  # LDAP Settings
  LDAP_AUTH_ENABLED: "false"
  LDAP_SERVER_URL: ""
  LDAP_TLS_INSECURE: "false"
  LDAP_TLS_CACERTFILE: ""
  LDAP_ENABLE_STARTTLS: "false"
  LDAP_BASE_DN: ""
  LDAP_USER_FILTER: ""
  LDAP_ADMIN_FILTER: ""
  LDAP_ID_ATTRIBUTE: "uid"
  LDAP_NAME_ATTRIBUTE: "name"
  LDAP_MAIL_ATTRIBUTE: "mail"
---
apiVersion: v1
kind: Secret
metadata:
  name: mealie-secrets
  namespace: utility
type: Opaque
stringData:
  POSTGRES_USER: "mealie"
  POSTGRES_PASSWORD: "YourSuperSecretPassword"
  OIDC_CLIENT_ID: "FromYourIdentityProviderSoftware"
  LDAP_QUERY_BIND: ""
  LDAP_QUERY_PASSWORD: ""
  SMTP_USER: "YourSMTPUser"
  SMTP_PASSWORD: "YourSuperSecretPassword"

02-storage.yaml

Even though we are using Postgres to store recipes, there are still some settings that are stored with the container. I defined a 50 MB config volume using Longhorn for this volume.

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  namespace: utility
  name: mealie-data
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 50Mi

03-deploy.yaml

The Deployment brings together the configuration and storage with the container image:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mealie
  namespace: utility
  labels:
    app: mealie
    app.kubernetes.io/name: mealie
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: mealie
  template:
    metadata:
      labels:
        app: mealie
        app.kubernetes.io/name: mealie
    spec:
      securityContext:
        runAsUser: 911
        runAsGroup: 911
      volumes:
        - name: mealie-data
          persistentVolumeClaim:
            claimName: mealie-data
      containers:
        - name: mealie
          image: ghcr.io/mealie-recipes/mealie:nightly
          imagePullPolicy: Always
          ports:
            - containerPort: 9000
          volumeMounts:
          - name: mealie-data
            mountPath: /app/data/
          envFrom:
            - configMapRef:
                name: mealie-configs
            - secretRef:
                name: mealie-secrets
          livenessProbe:
            httpGet:
              path: /g/home
              port: 9000
            initialDelaySeconds: 10
            periodSeconds: 5

04-service.yaml

The service will help expose the pod for use. I leverage ClusterIP with an Ingress, but you could use a LoadBalancer type to expose Mealie on an IP outside of your cluster directly.

---
kind: Service
apiVersion: v1
metadata:
  name: mealie-service
  namespace: utility
spec:
  selector:
    app: mealie
  ports:
  - protocol: TCP
    port: 9000
    targetPort: 9000
  type: ClusterIP

05-ingress.yaml

An Ingress is one way to expose your services and can allow you to use Cert Manager to create TLS certificates for your site as well. In the annotations: {} section I am defining some rate limiting and allowing gzip of the content. These are optional and could be omitted from the manifest.

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mealie-ingress
  namespace: utility
  annotations:
    cert-manager.io/cluster-issuer: 'letsencrypt'
    nginx.ingress.kubernetes.io/limit-rps: "10"
    nginx.org/location-snippets: |
      gzip on;
      gzip_vary on;
      gzip_min_length 1000;
      gzip_proxied any;
      gzip_types text/plain text/css text/xml application/xml text/javascript application/x-javascript image/svg+xml;
      gzip_disable "MSIE [1-6]\.";
spec:
  ingressClassName: nginx
  rules:
    - host: mealie.your.domain
      http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: mealie-service
              port:
                number: 9000
  tls:
    - hosts:
      - mealie.your.domain
      secretName: mealie-tls

build-mealie.sh

Now that we have prepared our manifests we need to deploy them to the cluster with kubectl. I create shell scripts for all my deployments so I can quickly redeploy if I make any adjustments. The below script does assume you have configured kubectl properly already.

#!/bin/bash

kubectl apply -f 00-utility-namespace.yaml \
              -f 01-config.yaml \
              -f 02-storage.yaml \
              -f 03-deploy.yaml \
              -f 04-service.yaml \
              -f 05-ingress.yaml

We can deploy the manifests for Mealie to the Kubernetes cluster by executing the following:

chmod 755 build-mealie.sh
./build-mealie.sh

I keep all my manifests, scripts, and helm charts in a private git repository for version control and archival storage. While it is certainly not required to deploy Mealie, it has made my life a little easier.