Skip to Content

Production Guide: Deploy Mealie with Docker Compose + Caddy + PostgreSQL on Ubuntu

Self-host your recipe manager and meal planner with automatic TLS and a PostgreSQL backend

Most teams eventually end up with recipe links scattered across browser bookmarks, shared Google Docs, and Slack threads. Mealie is a self-hosted recipe manager and meal planner that centralises your cookbook, generates shopping lists, and exposes a full REST API — so you can import from any URL, integrate with Home Assistant, and keep your household data on infrastructure you control. This guide walks you through a production-grade deployment of Mealie on Ubuntu using Docker Compose, Caddy as a reverse proxy with automatic TLS, and PostgreSQL as the database backend.

Architecture and flow overview

Incoming HTTPS requests arrive at Caddy on port 443. Caddy terminates TLS (via Let's Encrypt or ZeroSSL) and reverse-proxies to the Mealie container on its internal port 9000. Mealie stores all recipe data, user accounts, and shopping lists in a PostgreSQL container. An optional volume bind stores the Mealie data directory (images, backups, exports) on the host for easy backup and restore. All three services — Caddy, Mealie, and PostgreSQL — share a dedicated Docker bridge network and are managed by a single docker-compose.yml.

Internet → Caddy :443 (TLS termination)
                  ↓
           Mealie :9000 (app)
                  ↓
           PostgreSQL :5432 (data)

Prerequisites

  • Ubuntu 22.04 or 24.04 server with at least 1 vCPU and 1 GB RAM (2 GB recommended)
  • A public domain name pointed at the server's IP (A record)
  • Docker Engine 24+ and Docker Compose v2 plugin installed
  • Ports 80 and 443 open in UFW and any upstream firewall
  • openssl available for secret generation

Step-by-step deployment

1. Create the project directory

mkdir -p /opt/mealie && cd /opt/mealie
mkdir -p data pgdata

2. Generate PostgreSQL credentials

MEALIE_DB_USER=mealie
MEALIE_DB_NAME=mealie
MEALIE_DB_PASS=$(openssl rand -hex 24)
MEALIE_SECRET=$(openssl rand -hex 32)
echo "DB_PASS=$MEALIE_DB_PASS"
echo "SECRET=$MEALIE_SECRET"

Save these values — you will paste them into the .env file in the next step.

3. Create the environment file

cat > /opt/mealie/.env << 'EOF'
# PostgreSQL
POSTGRES_USER=mealie
POSTGRES_PASSWORD=CHANGE_ME_DB_PASS
POSTGRES_DB=mealie
POSTGRES_SERVER=postgres

# Mealie
SECRET_KEY=CHANGE_ME_SECRET
ALLOW_SIGNUP=false
DEFAULT_GROUP=Home
API_DOCS=false
TOKEN_TIME=720

# SMTP (optional — for email invites / password reset)
SMTP_HOST=
SMTP_PORT=587
SMTP_FROM_EMAIL=
SMTP_AUTH_STRATEGY=TLS
SMTP_USER=
SMTP_PASSWORD=
EOF

Replace CHANGE_ME_DB_PASS and CHANGE_ME_SECRET with the values generated in step 2.

4. Write the Docker Compose file

cat > /opt/mealie/docker-compose.yml << 'EOF'
version: "3.8"

services:
  postgres:
    image: postgres:15-alpine
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - mealie-net

  mealie:
    image: ghcr.io/mealie-recipes/mealie:latest
    restart: unless-stopped
    env_file: .env
    environment:
      DB_ENGINE: postgres
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_SERVER: ${POSTGRES_SERVER}
      SECRET_KEY: ${SECRET_KEY}
      ALLOW_SIGNUP: ${ALLOW_SIGNUP}
      DEFAULT_GROUP: ${DEFAULT_GROUP}
      API_DOCS: ${API_DOCS}
      TOKEN_TIME: ${TOKEN_TIME}
    volumes:
      - ./data:/app/data
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - mealie-net

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - mealie-net

volumes:
  caddy-data:
  caddy-config:

networks:
  mealie-net:
    driver: bridge
EOF

5. Write the Caddyfile

cat > /opt/mealie/Caddyfile << 'EOF'
recipes.example.com {
    reverse_proxy mealie:9000
    encode gzip
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options nosniff
        X-Frame-Options SAMEORIGIN
        Referrer-Policy strict-origin-when-cross-origin
    }
}
EOF

Replace recipes.example.com with your actual domain.

6. Open firewall ports and start the stack

ufw allow 80/tcp
ufw allow 443/tcp
cd /opt/mealie
docker compose up -d
docker compose logs -f --tail=50

Caddy will obtain a TLS certificate automatically on first startup. Watch the logs until you see Certificate obtained successfully.

7. Complete initial setup and create your admin account

Open https://recipes.example.com in your browser. On first run, Mealie runs database migrations and then presents a login page. Log in with the default credentials:

Immediately go to Profile → Change Password and set a strong password. Then go to Admin → Users and update the email address to match your own.

Configuration and secrets handling

All sensitive values live in /opt/mealie/.env. This file should be owned by root with mode 600:

chmod 600 /opt/mealie/.env
chown root:root /opt/mealie/.env

Never commit .env to version control. If you use a secrets manager such as Infisical or Vault, inject values at container start via environment substitution rather than writing them to disk. The SECRET_KEY value is used to sign JWT tokens — rotating it invalidates all active sessions, so treat it as a long-lived secret and store it in your backup alongside the database dump.

To disable public sign-up (recommended for team deployments), ensure ALLOW_SIGNUP=false in .env. Admin users can invite members via email or share an invite link from Admin → Invite Links.

Verification

# Confirm all containers are running
docker compose -f /opt/mealie/docker-compose.yml ps

# Check HTTPS and follow redirect
curl -I https://recipes.example.com

# Check database connectivity from the Mealie container
docker compose -f /opt/mealie/docker-compose.yml exec mealie \
  python3 -c "import psycopg2; psycopg2.connect(host='postgres', dbname='mealie', user='mealie', password='$(grep POSTGRES_PASSWORD /opt/mealie/.env | cut -d= -f2)'); print('DB OK')"

# Verify TLS certificate
echo | openssl s_client -connect recipes.example.com:443 2>/dev/null | openssl x509 -noout -dates

Expected results: all three services show Up, the HTTPS request returns 200 OK, and the certificate expiry is at least 89 days out.

Common issues and fixes

Caddy fails to obtain a certificate. Confirm your DNS A record resolves to the server's public IP with dig +short recipes.example.com. Caddy requires port 80 reachable from the internet for the ACME HTTP-01 challenge. Check docker compose logs caddy for specific error messages.

Mealie container exits with a database connection error on first start. The Mealie image runs migrations on startup and depends on PostgreSQL being ready. If the health check has not passed yet, Docker may start Mealie before the database accepts connections. Re-run docker compose up -d after confirming PostgreSQL shows healthy in docker compose ps.

Recipe images or backups are lost after a container update. Ensure the ./data volume is mounted as a bind mount on the host (as shown in the Compose file above) rather than a named volume. Named volumes survive docker compose down but can be harder to locate for backup scripts. A bind mount at /opt/mealie/data is easier to snapshot with standard tools.

Slow page loads after many recipes. Mealie builds a search index in the background. If you imported a large recipe collection, allow 2–5 minutes for indexing to complete. You can also increase the WEB_GUNICORN_WORKERS environment variable from the default of 1 to match your CPU count.

SMTP not sending emails. Verify the SMTP port is not blocked by your server firewall. Test the configuration directly from the Mealie admin panel under Admin → Email Settings → Test. For Gmail, use an App Password with SMTP_AUTH_STRATEGY=TLS on port 587.

FAQ

Can I import recipes directly from a website URL?

Yes. Mealie uses a recipe scraper library that supports over 500 cooking websites. Open Add Recipe → Import from URL and paste any recipe page link. Mealie extracts the title, ingredients, instructions, and cover image automatically. For sites that block scrapers, you can paste the raw recipe text using the OCR importer or the manual editor.

How do I back up and restore the Mealie instance?

Back up both the PostgreSQL database and the Mealie data directory. For the database:

docker compose -f /opt/mealie/docker-compose.yml exec postgres \
  pg_dump -U mealie mealie | gzip > /backups/mealie-$(date +%Y%m%d).sql.gz

For the data directory (images, exports): tar -czf /backups/mealie-data-$(date +%Y%m%d).tar.gz /opt/mealie/data. To restore, import the SQL dump into a fresh PostgreSQL container and restore the data directory before starting Mealie.

Can Mealie integrate with Home Assistant?

Yes. Mealie exposes a fully documented REST API. The community Home Assistant integration (hass-mealie) connects via your API token and allows you to display the week's meal plan on dashboards, create shopping list items from automations, and trigger recipe imports from voice assistants. Generate an API token under Profile → API Tokens.

Does Mealie support multiple households or groups?

Yes. Mealie has a Groups feature that lets you organise users into separate households, each with its own recipe library, meal plan, and shopping list. An administrator creates groups from Admin → Groups and assigns users. Members of one group cannot see another group's data by default, making it suitable for hosting multiple households on a single instance.

How do I upgrade Mealie to a newer version?

Pull the new image and recreate the container. Mealie applies any required database migrations on startup:

cd /opt/mealie
docker compose pull mealie
docker compose up -d mealie
docker compose logs -f mealie

Always take a database backup before upgrading in case a migration needs to be rolled back.

Is there a mobile app for Mealie?

Mealie ships a Progressive Web App (PWA) interface that installs on iOS and Android from the browser — tap Add to Home Screen in Safari or Chrome. For a native experience, third-party clients such as the Android app Mealiemate connect via the REST API. The PWA covers most daily workflows including meal planning, shopping lists, and recipe browsing from a phone.

Internal links

Talk to us

If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.

Contact Us

Production Guide: Deploy Gitea with Docker Compose + Caddy + PostgreSQL on Ubuntu
Self-host a lightweight, production-ready Git service with automatic TLS, SSH access, and PostgreSQL persistence