SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

Mealie for VPS: Run Your Own Recipe Manager

Run Mealie for your VPS with Docker Compose. Paste recipe links to get clean steps, then manage meal plans and shopping lists with nginx, TLS, and backups.

Wetin self-hosted recipe manager dey do

Self-hosted recipe manager dey keep your recipes for database on server wey you own, and Mealie na the one wey most households dey choose. You paste the address of recipe page, and Mealie go read the ingredients, the steps, the yield, and the cook time from am, then leave the story and advertising behind. Na the food go enter your collection.

The rest of the app small. You get weekly meal plan wey you fit drag recipes enter, plus shopping list wey dey build from that plan. Each person wey dey cook get im own login. Everything dey run inside one container, and e dey idle between requests, so modest VPS fit carry am without stress.

This guide dey use Docker Compose. If the words services: and volumes: new to you, read how Docker Compose files dey put together first, because everything wey follow na one compose file and four commands.

Docker Compose dey install Mealie

Mealie dey publish its images to GitHub container registry. As of July 2026, the current stable tag na v3.22.0. Pin one version instead of using latest: with latest, one docker compose pull for another day fit move you across database migration wey you never ready for.

sudo mkdir -p /srv/mealie
cd /srv/mealie
sudo nano docker-compose.yml
services:
  mealie:
    image: ghcr.io/mealie-recipes/mealie:v3.22.0
    container_name: mealie
    restart: always
    ports:
      - "127.0.0.1:9925:9000"
    deploy:
      resources:
        limits:
          memory: 1000M
    volumes:
      - mealie-data:/app/data/
    environment:
      ALLOW_SIGNUP: "false"
      PUID: 1000
      PGID: 1000
      TZ: Europe/Amsterdam
      BASE_URL: https://recipes.example.com

volumes:
  mealie-data:

Two lines deserve make you check dem before you start am.

The port dey written as 127.0.0.1:9925:9000, no be 9925:9000. The container dey listen on 9000 inside, while the host dey map 9925 to am. When you bind that map to the loopback address, nginx fit reach Mealie but internet no fit. Docker dey write its own rules inside the packet filter, so plain 9925:9000 fit dey reachable from outside even when your firewall talk say the port dey closed. Na important surprise to understand once: see why published Docker ports ignore ufw.

BASE_URL must be the exact public address wey you go use, with the scheme and no trailing slash. Mealie dey build password reset links and invitation links from am. Set am to http://localhost:9925, and the invitation wey you send your partner go get link wey only dey work for the server itself.

Start am and monitor the first boot.

sudo docker compose up -d
sudo docker compose logs -f mealie

The first start dey create the SQLite database and run the migrations, and e dey take some seconds. When the log settle and stop printing migration lines, check the app locally.

curl -I http://127.0.0.1:9925

A 200 OK mean say the app don come up. Connection refused mean say the container no dey run: run sudo docker compose ps and read the exit code. Container wey stop with code 137 don get kill because e pass the 1000M memory limit, and this fit happen for the smallest plans.

First login, and how to close open signup

The default account na changeme@example.com with password MyPassword. Use am log in, then change both immediately, because dem print that pair for the documentation, so every scanner get am.

ALLOW_SIGNUP: "false" for the compose file na deliberate. When signup dey open, anybody wey find the address fit create account for your recipe box. When you close am, you go add people from the admin area. The system go generate invitation link wey you go send dem yourself. That link dey build from BASE_URL, na why the value important. If you later run several apps for the same server and want use one password for all of dem, Mealie fit send login go external identity provider like self-hosted Authentik instance.

Mealie dey group users into one household. Everybody for one household dey share the recipe collection, meal plan, and shopping list. Na wetin family need be that. Different households for the same server dey keep separate collections. Na wetin flatshare need be when nobody agree about anchovies.

The importer, naim why you dey run this

Open the recipe collection, choose to create recipe from URL, then paste link. Mealie go fetch the page and look for structured recipe data, wey be machine-readable block wey most recipe sites dey add for search engines. If that block dey there, the import clean and immediate.

You fit also import from image or plain text wey you paste, including photograph of cookbook page. Those ones dey use slower process and you need check dem afterwards, because handwritten fraction easy to read wrong.

Bulk imports dey run from the same screen: paste list of addresses, one for each line, and Mealie go process dem for background. Collection of two hundred bookmarks fit move across for one sitting.

Meal plan and shopping list

The meal planner na calendar. Drag recipe put am for one day and e don plan. Shopping list go collect ingredients from all the recipes wey you plan, put dem for one list, and combine duplicate items. So, if two recipes need onions, e go show one line instead of two.

The list na live page wey dey your phone as you dey shop. Because na your own server dey hold am, everybody for the house go see the same list at the same time. If one person tick milk, e go disappear from the other person's screen too.

Put nginx and TLS for front

Mealie dey speak plain HTTP and e no get certificate handling by itself. Make nginx for front handle transport layer security (TLS). First point DNS A record go your server, because certificate step go verify that name.

sudo apt update && sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/mealie
server {
    listen 80;
    server_name recipes.example.com;

    client_max_body_size 64M;

    location / {
        proxy_pass http://127.0.0.1:9925;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
sudo ln -s /etc/nginx/sites-available/mealie /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

nginx -t printing syntax is ok and test is successful na the gate. Only reload after e pass, because reloading broken config go leave the old one running and hide the mistake until the next restart.

client_max_body_size 64M dey there because nginx default na 1 MB. Uploading recipe photo or restoring backup through browser fit send body wey pass that size. Without that line, nginx go return 413 Request Entity Too Large, no be Mealie, so application log no go show anything.

Then issue the certificate. That step and the renewal timer dey covered for issuing a Let's Encrypt certificate for nginx with certbot.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d recipes.example.com

Certbot go rewrite the server block to listen on 443 and add redirect from port 80. Load the site over https:// and confirm say browser accept the certificate. If Mealie load but its own links send you go http://, then BASE_URL still dey say http and you need correct am. After that, run sudo docker compose up -d to recreate the container with the new value.

Serving Mealie under subpath like example.com/recipes no work, because frontend no fit serve from subpath. Use subdomain.

Backups, and wetin restore really dey do

Everything wey Mealie get dey inside /app/data/ for the container, and na the mealie-data volume be that. Copy that volume, and you don copy the recipes, images, and database together.

sudo docker volume ls
sudo docker compose stop mealie
sudo docker run --rm -v mealie_mealie-data:/data -v "$PWD":/backup \
  alpine tar czf /backup/mealie-data.tgz -C /data .
sudo docker compose start mealie

The volume name get project name as prefix, and project name na the directory wey hold the compose file. From /srv/mealie, the volume na mealie_mealie-data, na why the first command be docker volume ls: use the name wey e print, no be the name for this guide. E important make you stop the container first, because SQLite dey write often enough; live copy fit restore as unreadable data.

Mealie get im own backup page for the admin area too. E dey write portable archive wey hold the database as JSON together with your images. Use am when you dey move between servers, because e fit survive version change wey raw file copy fit no survive. Restoring one destructive by design: e go delete the current database before e load the archive, and you no fit undo am. E go log you out when e finish.

Neither copy be backup if e still dey the same server. Push the archive go another place on a schedule. Na wetin encrypted off-server backups with restic dey do.

Update Mealie

cd /srv/mealie
sudo nano docker-compose.yml
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs -f mealie

Raise the pinned version for the file, then pull and recreate am. Migrations go run when the new image start for the first time. Make copy of the volume before major version change, because if migration fail halfway, e go leave database wey the previous image no go fit open again. Read the release notes for all the versions between your current version and the new one.

When importer dey fail

Some sites no dey publish structured recipe data at all, so Mealie go import title with ingredient list wey empty. You no fit configure this problem comot. Paste recipe text manually instead.

Other failures dey come from bot protection wey dey in front of recipe site. E dey answer Mealie with challenge page instead of the recipe. Mealie don already dey act like browser and dey rotate user agent to reduce this problem. If site still refuse, documented options na to send scraper through proxy wey get better address reputation, or run FlareSolverr instance wey go solve challenge inside real browser. Both options na optional, and you go set both through environment variables for the container.

If import fail because your server no fit reach the site at all, na different problem be that. Test am from the server with curl -I https://the-site.example/recipe and read status line before you blame scraper.

Wia e fit work

Mealie na good first self-hosted application for household, because people wey dey live with you go use am without you asking dem. E be similar kind of work as running your own photo library with Immich, but e lighter well-well, and e dey inside the wider list of things wey worth self-hosting this year. One small server fit hold both. Immich no be the only option for that second work, and if you never decide, the memory floors and backup commands of PhotoPrism and Immich differ enough to make am worth reading before you use the remaining disk space for something else.

FAQ

Why importing recipe URL dey fail?

Two common causes dey. Either the page no publish structured recipe data, so the scraper no find anything and you get title without ingredients, or bot-protection layer wey dey front of the site return challenge page instead of the recipe. For the second case, you fit point Mealie to proxy wey get better address reputation, or to self-hosted FlareSolverr instance wey go solve the challenge inside real browser. Confirm say your server fit reach the page at all with curl -I before you change anything.

I need PostgreSQL, or SQLite enough?

SQLite enough for household, and na the default. Move go PostgreSQL when data directory dey for network attached storage, because SQLite for network filesystem dey produce locked-database errors and fit corrupt the file. Restores under PostgreSQL need the database user to be superuser, because the restore dey delete everything before e load the archive.

I fit run Mealie without domain name?

Yes, for your own network. Set BASE_URL to the address wey you go actually type, such as http://192.168.1.20:9925, and skip nginx. Invitation and password reset links dey built from BASE_URL, so wrong value go produce links wey nobody else fit open. No expose am to internet over plain HTTP, because login go then send in the clear.

How I go give my family their own logins?

Leave ALLOW_SIGNUP set to "false" and add people from the admin area, wey go produce invitation link you go send them. Put everybody wey dey share kitchen for the same household so dem fit share recipes, meal plan and shopping list. Separate households for one server dey keep separate collections.

Wetin go happen to my recipes if I stop running Mealie?

Dem go comot. The admin backup dey write your data as JSON, and Mealie fit also export recipes as plain markdown files, wey remain readable for any text editor without any software at all. Make one export before you need am and check say you fit open am.