All articles
TutorialsApr 14, 2026 · 21 min read

Self-Host Mealie on a VPS for a Family Recipe Manager

Self-Host Mealie on a VPS for a Family Recipe Manager

Recipe sites have gotten unbearable. Forty paragraphs of life story, three video ads, a newsletter modal, and the actual ingredient list buried somewhere in the middle. Saving recipes to a phone notes app works for one or two, but it falls apart the moment two people in the same household want to plan dinner together.

Mealie fixes both problems. It's an open-source self-hosted recipe manager with a slick Vue front end, a real database underneath, and a recipe importer that takes a URL from any cooking blog and pulls out just the recipe. It also handles meal planning, shopping lists, multiple households, and ships a mobile PWA so it feels like a native app on a phone.

This guide walks through a production-ready install on a VPS: Docker Compose, PostgreSQL, Caddy for automatic HTTPS, and a backup script that covers both the database and the uploaded images.

Leave `ALLOW_SIGNUP=false` for any Mealie instance reachable on the public internet. With signups on, anyone who finds your `BASE_URL` can register an account and start poking around your household. Invite family members manually from the admin panel instead.

TL;DR

  • Install Docker and Docker Compose on a fresh VPS
  • Point a subdomain like recipes.example.com at your server
  • Run Mealie + PostgreSQL + Caddy with a single docker-compose.yml
  • Set ALLOW_SIGNUP=false and create the first user with DEFAULT_EMAIL
  • Use the URL importer to pull recipes from cooking blogs in one click
  • Back up /app/data plus a daily pg_dump

Total time: about 20 minutes.

What You Need

  • A VPS with at least 1 GB RAM (2 GB is more comfortable) running Ubuntu 22.04 or 24.04
  • A domain name you can add DNS records to
  • Ports 80 and 443 open to the internet (required by Let's Encrypt)
  • Root or sudo access

Mealie is light. Idle memory is around 200 MB and a small VPS handles a household of four or five with thousands of recipes without breaking a sweat.

Step 1: Point a Subdomain at Your VPS

In your DNS provider, add an A record:

recipes.example.com → YOUR_VPS_IPV4

Add an AAAA record for IPv6 if you use it. Verify propagation:

dig +short recipes.example.com

DNS needs to resolve before Caddy can fetch a Let's Encrypt certificate.

Step 2: Install Docker and Docker Compose

On a fresh Ubuntu server:

sudo apt update sudo apt install -y ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Confirm everything is wired up:

docker --version docker compose version

Step 3: Open the Firewall

sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable

Caddy needs port 80 for the ACME HTTP challenge and 443 for HTTPS.

Step 4: Create the Project Directory

sudo mkdir -p /opt/mealie cd /opt/mealie sudo mkdir -p mealie-data postgres-data caddy-data caddy-config

Everything persistent lives under /opt/mealie. Back up that one directory plus a database dump and you can rebuild the stack on any new box.

Step 5: Generate Secrets

Two secrets to mint before writing the compose file:

# PostgreSQL password openssl rand -base64 32 # Initial admin password for the first user openssl rand -base64 24

Save both somewhere safe. The admin password is what you'll use the first time you log in.

Step 6: Write the Environment File

Create /opt/mealie/.env:

# Domain BASE_URL=https://recipes.example.com # Database POSTGRES_USER=mealie POSTGRES_PASSWORD=REPLACE_WITH_POSTGRES_PASSWORD POSTGRES_DB=mealie # Mealie security ALLOW_SIGNUP=false DEFAULT_GROUP=Family [email protected] DEFAULT_PASSWORD=REPLACE_WITH_INITIAL_ADMIN_PASSWORD # Locale and timezone TZ=Europe/Berlin DEFAULT_LOCALE=en-US

Lock it down so only root can read it:

sudo chmod 600 /opt/mealie/.env

A note on the variables:

  • BASE_URL must match the public URL exactly, including https://. Mealie uses it to build absolute links in invite emails and the PWA manifest.
  • ALLOW_SIGNUP=false is the single most important setting in this file. Keep it off.
  • DEFAULT_EMAIL and DEFAULT_PASSWORD only seed the first user on first boot. After that they are ignored. Change the password from the UI once you log in.

Step 7: Write the Compose File

Create /opt/mealie/docker-compose.yml:

services: postgres: image: postgres:16 container_name: mealie-postgres restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - ./postgres-data:/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 container_name: mealie restart: unless-stopped depends_on: postgres: condition: service_healthy environment: TZ: ${TZ} BASE_URL: ${BASE_URL} ALLOW_SIGNUP: ${ALLOW_SIGNUP} DEFAULT_GROUP: ${DEFAULT_GROUP} DEFAULT_EMAIL: ${DEFAULT_EMAIL} DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} DEFAULT_LOCALE: ${DEFAULT_LOCALE} DB_ENGINE: postgres POSTGRES_SERVER: postgres POSTGRES_PORT: "5432" POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - ./mealie-data:/app/data networks: - mealie-net caddy: image: caddy:2 container_name: mealie-caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./caddy-data:/data - ./caddy-config:/config networks: - mealie-net networks: mealie-net:

A couple of details worth flagging:

  • Mealie's container port 9000 stays internal. Caddy reaches it over the Docker network, so there's no need to expose it on the host.
  • The /app/data volume holds uploaded recipe images, backups, and the search index. Losing it means rescraping every recipe.
  • ghcr.io/mealie-recipes/mealie:latest works fine for hobby installs. Pin to a specific tag like :v2.5.0 once you have a setup you like, so an automatic pull doesn't surprise you.

Step 8: Write the Caddyfile

Create /opt/mealie/Caddyfile:

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

Caddy will request and renew a Let's Encrypt certificate automatically. No further configuration.

Step 9: Start the Stack

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

Watch the logs until Caddy reports the certificate was issued and Mealie finishes its first migration. Then open https://recipes.example.com. You should land on the login screen.

Sign in with the email and password you set in DEFAULT_EMAIL / DEFAULT_PASSWORD. The first thing to do once you're in: open the user menu, change the password, and update the display name.

Step 10: Create a Household and Invite Family Members

Mealie's data model has three layers: users, households, and groups. A group is a tenant (think Family), a household is a planning unit inside that group (Main House, Lake Cabin, Grandma), and users belong to one or more households.

For most families, one group with one or two households is plenty.

To invite people:

  1. Click your avatar, go to Admin then Users.
  2. Click Invite User, fill in the email, and pick the household.
  3. Mealie generates a one-time invite link. Send it over Signal, iMessage, or whatever the family uses.
  4. They open the link, set a password, and they're in.

This is the supported way to add accounts when ALLOW_SIGNUP=false. No public registration form, no bots probing your subdomain.

Step 11: Import Your First Recipe From a URL

The headline feature. Find any recipe on a normal cooking blog and copy the URL. In Mealie:

  1. Click the green + button, choose Import.
  2. Paste the URL into the From a Website (URL) box.
  3. Hit Import.

Mealie scrapes the page using the open recipe-scrapers library, pulls out the title, ingredients, instructions, hero image, prep time, cook time, yield, and any nutrition data. Most well-formatted blogs come through clean. Sites with JSON-LD schema (which is the majority) work especially well.

After import, edit the recipe to clean up anything weird, add your own tags (weeknight, kid-friendly, vegetarian), and assign a category. Mealie's tagging is the killer feature for finding "what can we make tonight" later.

Step 12: Meal Planning and Shopping Lists

Open Meal Plans in the sidebar. The planner is a calendar grid: drag a recipe onto a day, mark it as breakfast, lunch, dinner, or snack, and Mealie tracks the schedule per household.

Shopping lists are linked. Open a planned recipe, click Add Ingredients to Shopping List, and Mealie aggregates ingredients across recipes (so two recipes calling for an onion become "2 onions" on one line). The list syncs in real time across devices, which is the part that makes Mealie click for two-cook households.

Useful planning patterns:

  • Plan a week on Sunday, generate the shopping list, then both adults can check items off in the store from their phones.
  • Tag recipes by season or by who-cooks-what, then filter the recipe page when you sit down to plan.
  • Use Random Meal in the planner if Sunday-night-decision-fatigue is real. Lock dietary tags so the random pick respects them.

Step 13: Install the Mobile PWA

Mealie ships as a Progressive Web App. There's no separate iOS or Android binary; the website is the app.

On iPhone, open https://recipes.example.com in Safari, tap the share button, then Add to Home Screen. On Android, Chrome offers an Install app prompt automatically, or you can pick Install app from the menu.

The PWA runs in its own window without browser chrome, has a proper app icon, and works offline for already-loaded recipes. Push notifications aren't available out of the box, but for "what's for dinner" purposes the PWA is indistinguishable from a native app.

Step 14: Back Up Mealie

Two things need backing up: the /app/data directory (uploaded images, the search index, internal backups) and the PostgreSQL database (recipes, plans, users, ratings).

Create /usr/local/bin/mealie-backup.sh:

#!/usr/bin/env bash set -euo pipefail BACKUP_DIR="/var/backups/mealie" DATE="$(date +%F)" mkdir -p "$BACKUP_DIR" docker exec mealie-postgres \ pg_dump -U mealie -d mealie \ | gzip > "$BACKUP_DIR/mealie-db-$DATE.sql.gz" tar -czf "$BACKUP_DIR/mealie-data-$DATE.tar.gz" -C /opt/mealie mealie-data find "$BACKUP_DIR" -name "mealie-*.tar.gz" -mtime +14 -delete find "$BACKUP_DIR" -name "mealie-*.sql.gz" -mtime +14 -delete

Make it executable and schedule it nightly:

sudo chmod +x /usr/local/bin/mealie-backup.sh echo "20 3 * * * root /usr/local/bin/mealie-backup.sh" | \ sudo tee /etc/cron.d/mealie-backup

For off-site safety, sync the backup directory to S3, Backblaze B2, or another VPS with rclone on the same schedule. Recipes you collected over a decade are not the kind of thing you want to rebuild from memory.

Mealie also has a built-in **Backup** menu under Settings that exports a single zip with recipes and metadata. It's great for migrating between hosts, but it doesn't capture uploaded images or PostgreSQL roles. Keep the script above as your real backup; treat the in-app export as a portable extra.

Step 15: Upgrade Mealie Safely

Mealie ships frequent updates. The safe upgrade path:

cd /opt/mealie sudo /usr/local/bin/mealie-backup.sh sudo docker compose pull sudo docker compose up -d

Always take a backup before upgrading, especially across major versions. Database migrations occasionally need a manual touch on big releases; the changelog calls those out.

Troubleshooting

Importer fails on certain blogs. A handful of sites hide content behind Cloudflare's bot mitigation or paywall the recipe portion. The scraper sees a challenge page and bails. Workarounds: copy the recipe and paste it into the Create from Scratch form, or use the browser bookmarklet that ships with Mealie to scrape from your already-authenticated browser session.

Images do not load after a restart. Almost always a volume permissions issue. Mealie runs as UID 911 by default. Fix it once with sudo chown -R 911:911 /opt/mealie/mealie-data then docker compose restart mealie.

Search returns nothing for recipes you know exist. The full-text search index lives in /app/data and can fall behind after a bulk import or restore. Trigger a rebuild from Admin then Maintenance then Reindex Recipes. It runs in seconds for a few thousand recipes.

Can't log in after an upgrade. Check the Mealie container logs first; a database migration may have failed mid-way. Bring the stack down with docker compose down, restore the latest mealie-db-*.sql.gz into PostgreSQL, then pin the previous image tag in docker-compose.yml and bring it back up. Open the changelog before retrying the upgrade.

Going Further

  • Try Tandoor as an alternative if you want sharper meal planning, supermarket-aware shopping lists, and tighter inventory tracking. The trade-off is a slightly less polished mobile experience.
  • Wire up Apprise notifications so meal plan reminders ping a Telegram chat or a Discord channel before dinner prep.
  • Hook Mealie into Home Assistant with the community integration. The "what's for dinner" card on a kitchen tablet is a nice quality-of-life win.
  • Run multiple households in one group for an extended family share. Each household keeps a separate meal plan and shopping list while sharing the recipe library.

Self-hosting Mealie turns a chaotic mess of bookmarked recipes and screenshotted ingredient lists into one searchable family cookbook that nobody else owns.


Looking for a VPS that's ready for stacks like this? Our Linux plans come with NVMe storage and IPv6 out of the box. See the options.