Mealie behind Caddy

Verified · Caddy 2.8

Config + gotchas checked against the official Mealie docs and the issues people actually hit · 2026-06-07

A Caddy reverse-proxy setup for Mealie, with automatic HTTPS on mealie.example.com. Copy it, swap the domain — the gotchas that trip people up are solved below.

New to reverse proxies? Start with the Caddy + Docker guide.

Caddyfile
# Caddyfile for Mealie
# Replace mealie.example.com with your real domain. Caddy issues TLS automatically.

mealie.example.com {
	encode zstd gzip
	reverse_proxy mealie:9000
}

Replace mealie.example.com with your domain. Your DNS A record must already point at the server.

Gotchas

What trips people up

Share links, password-reset emails, or OIDC redirects come back as http:// even though the site is HTTPS.

Mealie builds absolute URLs from `BASE_URL`, not the request. Set `BASE_URL=https://mealie.example.com` in `.env`. Recent images honor X-Forwarded-Proto (Caddy sends it); older ones also need the uvicorn flag `--forwarded-allow-ips="*"`.

Mealie works on the LAN but 502s or misbehaves behind the domain.

Don't publish port 9000 to the internet — reach Mealie only via the HTTPS proxy. Make sure the mealie container and Caddy share the `proxy` network and you're hitting the https domain.

Deploy

How to deploy

  1. Create the shared proxy network once: `docker network create proxy`.
  2. Set `BASE_URL=https://mealie.example.com` in `.env`.
  3. Start the stack: `docker compose up -d`, then point mealie.example.com at the server (ports 80/443 reachable for the cert).

Runnable

Full runnable stack

Mealie + Caddy + the shared network, in one compose file — so it runs on the first try, not after you stitch the proxy in yourself. Save the Caddyfile above next to it.

docker-compose.yml (app + Caddy)
# docker-compose.yml — Mealie + Caddy, ready to run.
# Pin caddy to a digest for production: caddy:2.8-alpine@sha256:<digest>
# 1. Save the Caddyfile above as ./Caddyfile
# 2. docker network create proxy
# 3. docker compose up -d
services:
  mealie:
    image: ghcr.io/mealie-recipes/mealie:v3.19.2@sha256:f68e959bf66f4f458893ea58facac71690fe6f2ac7a31466b5cecb41b4e99c02
    restart: unless-stopped
    depends_on:
      - mealie-postgres
    environment:
      ALLOW_SIGNUP: "false"
      BASE_URL: https://${MEALIE_HOST}
      TZ: ${TZ}
      DB_ENGINE: postgres
      POSTGRES_SERVER: mealie-postgres
      POSTGRES_PORT: "5432"
      POSTGRES_USER: mealie
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: mealie
    volumes:
      - mealie-data:/app/data
    networks:
      - default
      - proxy
  mealie-postgres:
    image: postgres:16-alpine@sha256:16bc17c64a573ef34162af9298258d1aec548232985b33ed7b1eac33ba35c229
    restart: unless-stopped
    environment:
      POSTGRES_USER: mealie
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: mealie
      POSTGRES_INITDB_ARGS: --data-checksums
    volumes:
      - mealie-postgres-data:/var/lib/postgresql/data
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U mealie -d mealie
      interval: 10s
      timeout: 5s
      retries: 5
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - proxy
volumes:
  mealie-data: {}
  mealie-postgres-data: {}
  caddy-data: {}
  caddy-config: {}
networks:
  proxy:
    external: true

Full Mealie stack (compose + env + docs)