Contents

Mealie: Recipes You Actually Own

A recipe manager that scrapes the web, strips the essay, and answers to you

Contents

The modern recipe page is an act of hostility. You arrive wanting to know how much flour. Between you and that number sit: a cookie banner, a newsletter modal, an autoplaying video that follows you down the page, 1,400 words about the author’s grandmother in Tuscany, three affiliate blocks, and a comment section in which forty people report having substituted every ingredient and then rated the result four stars. The flour is down there somewhere. It is 240 grams. It took eleven seconds of scrolling and four megabytes of JavaScript to learn that.

Every part of that page is deliberate. It is SEO working precisely as designed. Google rewards dwell time and word count, so the recipe — the actual payload, forty lines of text — gets buried under the machinery that makes the page rank. Everyone involved knows this is absurd and nobody can defect first.

Mealie is the defection. It is a self-hosted recipe manager that takes a URL, scrapes the structured data out of it, throws the grandmother in the bin, and stores the forty lines. Once a recipe is in there it is yours: searchable, taggable, printable, offline, and immune to the site going down, going paywalled, or quietly changing the oven temperature.

I have run it for over a year with about 300 recipes in it. This is what it does well, what it does badly, and whether the container earns its RAM.

Why the scraper works at all

Advertisement

The thing that makes Mealie possible is a standard almost nobody talks about: schema.org Recipe markup. Google made it effectively mandatory a decade ago — if you want the little photo-and-star-rating card in search results, you embed a JSON-LD block in your page describing the recipe in machine-readable form. Ingredients, instructions, yield, prep time, cook time, all properly typed.

So every recipe site that cares about traffic — which is all of them — is already publishing a clean, structured version of the recipe, sitting in a <script type="application/ld+json"> tag underneath the grandmother. It is right there in the HTML.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "@context": "https://schema.org",
  "@type": "Recipe",
  "name": "Slow-Roast Shoulder of Lamb",
  "recipeYield": "6 servings",
  "prepTime": "PT20M",
  "cookTime": "PT5H",
  "recipeIngredient": [
    "2 kg lamb shoulder, bone in",
    "6 cloves garlic, sliced",
    "2 tbsp olive oil"
  ],
  "recipeInstructions": [
    {"@type": "HowToStep", "text": "Heat the oven to 160C fan."}
  ]
}

Mealie fetches the page, parses that block, maps it to its own model, downloads the image, and stores the result in Postgres. The 1,400 words about Tuscany are never even read. The irony is thick and entirely delicious: the SEO apparatus that ruined recipe pages is the same apparatus that makes them trivially strip-mineable.

When a site has good markup, the scrape is perfect and takes about two seconds. When it does not, you get a mess — which is the subject of the troubleshooting section, because it happens more than the project’s demo video suggests.

The stack

Mealie ships as a single container with an optional Postgres backend. It will run on SQLite, and for one user that is genuinely fine. I use Postgres because I already have an instance and because the full-text search over 300 recipes is noticeably snappier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
services:
  mealie:
    image: ghcr.io/mealie-recipes/mealie:v1.12.0
    container_name: mealie
    depends_on:
      - mealie-db
    environment:
      - ALLOW_SIGNUP=false
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Copenhagen
      - BASE_URL=https://recipes.mylab.local
      - DB_ENGINE=postgres
      - POSTGRES_USER=mealie
      - POSTGRES_PASSWORD_FILE=/run/secrets/mealie_db_password
      - POSTGRES_SERVER=mealie-db
      - POSTGRES_PORT=5432
      - POSTGRES_DB=mealie
    volumes:
      - /srv/appdata/mealie/data:/app/data
    secrets:
      - mealie_db_password
    ports:
      - "127.0.0.1:9925:9000"
    deploy:
      resources:
        limits:
          memory: 1G
    restart: unless-stopped

  mealie-db:
    image: postgres:16-alpine
    container_name: mealie-db
    environment:
      - POSTGRES_USER=mealie
      - POSTGRES_PASSWORD_FILE=/run/secrets/mealie_db_password
      - POSTGRES_DB=mealie
    volumes:
      - /srv/appdata/mealie/pgdata:/var/lib/postgresql/data
    secrets:
      - mealie_db_password
    restart: unless-stopped

secrets:
  mealie_db_password:
    file: /srv/secrets/mealie_db_password.txt

Note the port binding to 127.0.0.1 — Mealie’s authentication is fine, and I would still rather nothing on the LAN can reach it directly. TLS and access control come from the reverse proxy in front, following the pattern I laid out in Reverse Proxy Done Right: Automatic HTTPS with Caddy.

ALLOW_SIGNUP=false matters. Mealie’s default in some versions has been to allow open registration, and if you expose it and forget that flag, you will eventually find a stranger’s meal plan in your database. Create users manually from the admin panel.

First run gives you [email protected] / MyPassword as the default credentials, which is exactly as safe as it sounds. Change them before you do anything else, including reading the rest of this paragraph.

Living with it

Advertisement

The workflow that stuck, after a year of adjusting:

Import by URL from the phone. Mealie has a bookmarklet and the API takes a URL directly, so a recipe found on the bus is in the library before the bus stops. This is the feature that decides whether the whole thing gets used, because a recipe manager you have to sit at a desk to feed is a recipe manager that stays empty.

1
2
3
4
5
$ curl -s -X POST https://recipes.mylab.local/api/recipes/create/url \
    -H "Authorization: Bearer $MEALIE_TOKEN" \
    -H 'Content-Type: application/json' \
    -d '{"url": "https://example.com/slow-roast-lamb", "includeTags": true}'
"slow-roast-shoulder-of-lamb"

It returns the new slug. That is the entire API for the most common operation, and it means you can wire imports into anything — a shared note, a Telegram bot, a browser extension.

Tag ruthlessly on the way in. Mealie’s search is good, and it is only as good as your tags. I use three axes: protein (lamb, lentils), effort (weeknight, project), and origin (nan, internet, book). Three axes covers every retrieval question I actually ask.

Meal planner, then shopping list. Drag recipes onto a calendar for the week; Mealie aggregates the ingredients into a shopping list, deduplicating where the parser understood the units. The list is a shared, checkable web page that both phones can see. This is the feature that converted the rest of my household from sceptics to users, and household adoption is the only metric that matters for software that lives in a kitchen.

The ingredient parser is the soft underbelly. Mealie tries to decompose "2 tbsp olive oil, plus extra for drizzling" into quantity 2, unit tbsp, food “olive oil”, note “plus extra for drizzling”. It ships two parsers: a fast regex-based one (NLP off) and a CRF model. The CRF model is better and pulls in a chunk of extra memory. Neither is reliable on British recipe prose, which is full of things like "a knob of butter" and "1 x 400g tin". Expect to hand-correct maybe one ingredient in eight if you want the shopping-list maths to work.

If you want the parser at all, the CRF version is the one to enable:

1
      - INGREDIENT_PARSER=nlp

If you do not care about aggregated shopping lists, leave it off, save the RAM, and treat ingredients as strings. Plenty of people do.

The meal planner deserves its own note. Drag recipes onto a calendar and Mealie will also serve today’s plan through a public link, which means a tablet in the kitchen can show “what are we eating” without anyone logging in. There is a /api/groups/mealplans/today endpoint too, so a Home Assistant dashboard can display it alongside everything else. Small feature, disproportionate household goodwill.

Cookbooks are saved filters with a nicer name. Define one as “tag = weeknight AND tag = lentils” and it stays current as you add recipes. I have four: weeknight, weekend project, things the children will eat, and things to cook when someone is ill. That last one has never once been consulted in a state of good health and has been invaluable twice.

Troubleshooting

The scrape returns an empty recipe with a correct title. The site has no schema.org markup, or hides it behind JavaScript rendering. Mealie fetches HTML; it does not run a browser. Check what is actually there:

1
2
$ curl -s https://example.com/some-recipe | grep -o 'application/ld+json' | head -1
application/ld+json

No output means no markup and no scrape. Your options are Mealie’s “create from HTML/text” path, where you paste the ingredient block and it does its best, or typing it in. Sites that render recipes purely client-side are a small and shrinking minority, because they also lose their Google rich card, which their traffic team will not tolerate for long.

Images fail to download but the recipe imports. Almost always a hotlink protection or CDN 403. The recipe is fine; add a photo yourself or ignore it. Not worth fixing.

502 from the reverse proxy after an update. Mealie runs database migrations on start, and on a large library the first boot after a major version bump can take a couple of minutes with the port already open. Read the logs before you panic:

1
2
$ docker logs -f mealie | grep -i alembic
INFO  [alembic.runtime.migration] Running upgrade 09aba125b57a -> 44e8d670719d

Let it finish. Restarting the container mid-migration is how you find out whether your backups work.

Postgres connection refused on first boot. depends_on waits for the container to start, not for Postgres to be ready to accept connections. Mealie usually retries; when it does not, restarting it once fixes it permanently. A proper healthcheck on the database service is the tidy answer.

Memory creeping past a gigabyte. The CRF ingredient parser loads its model per worker. If you have set a worker count above 1 on a machine with modest RAM, you have multiplied that model. Leave the worker count at the default for home use — you have four users, not four thousand.

Backups, and the thing people get wrong

Mealie has a built-in backup function in the admin panel that produces a zip of the database and the recipe images. It works, it is one click, and it is the correct restore path because it handles the schema version properly.

It is also useless if you never press it. Automate it:

1
2
3
$ curl -s -X POST https://recipes.mylab.local/api/admin/backups \
    -H "Authorization: Bearer $MEALIE_TOKEN"
{"message":"A backup has been created"}

Then let your normal backup job sweep /srv/appdata/mealie/data/backups offsite. Dumping the Postgres volume directory with cp while the database is running produces a file-shaped object of no value; if you want database-level backups do them properly with pg_dump, which I went through in Database Backups Done Right: Dumps, WAL, and PITR.

1
$ docker exec mealie-db pg_dump -U mealie -Fc mealie > /srv/backup/mealie-$(date +%F).dump

Both belong in a 3-2-1 arrangement — the mechanics of which I have laid out in Restic + Rclone: Offsite Backups That Survive Your Own Mistakes. A recipe library is a strange thing to lose, because unlike photos there is no emotional alarm attached to it. You simply discover, on a Tuesday in eighteen months, that the good dal is gone.

Getting your recipes back out

Advertisement

The entire argument for self-hosting a recipe manager collapses if the recipes are trapped in it. So before you put 300 recipes in, confirm you can get 300 recipes out. Mealie can, in two ways.

Every recipe has a JSON export and a zip export containing the recipe plus its image, and both are reachable from the API, which means the whole library is one loop away:

1
2
3
4
5
$ curl -s "https://recipes.mylab.local/api/recipes?perPage=1000"     -H "Authorization: Bearer $MEALIE_TOKEN"   | jq -r '.items[].slug'   | while read -r slug; do
      curl -s "https://recipes.mylab.local/api/recipes/$slug"         -H "Authorization: Bearer $MEALIE_TOKEN" > "export/$slug.json"
    done
$ ls export | wc -l
301

Those JSON files carry the schema.org fields more or less intact, which means any future tool that understands Recipe markup can eat them. That is the real portability guarantee here, and it comes from Mealie having picked an existing standard as its data model instead of inventing one.

The second route is Mealie’s own zip backup, which is the right tool for moving between Mealie instances and the wrong tool for moving to anything else, since it encodes internal database structure.

I ran the export loop above once as a drill, opened forty files at random, and confirmed the ingredients were there. It cost twenty minutes and it is the difference between owning a library and renting one from a container. Do it before you are invested, while walking away is still cheap.

Mealie against the alternatives

Tandoor is the obvious rival, and it is the more capable application: better ingredient handling, proper meal-plan maths, a genuine unit-conversion system, superior handling of recipes that reference other recipes. It is also heavier, fussier to set up, and its interface has the distinct aroma of software designed by someone who enjoys spreadsheets. I compared the two at length in Tandoor: When Your Recipe Collection Outgrows Bookmarks. If your household contains an engineer and nobody else, run Tandoor.

Paprika is the commercial answer, costs about a fiver per platform, and is genuinely excellent. It syncs through their cloud. If that trade is acceptable to you, it is the least effort by a wide margin, and there is no shame in it.

A folder of markdown files is the answer nobody wants to hear and the one that never breaks. It has no shopping list and no scraper. It will still open in thirty years.

The verdict

Mealie’s pitch is a pleasant interface, a scraper that works on maybe eighty-five per cent of sites, a meal planner other people in the house will voluntarily use, and about 400MB of RAM. It is a good, well-made piece of software with an ingredient parser that is weaker than the marketing implies and a v1 that has been stable enough to trust.

Run it if you cook regularly from internet recipes and it annoys you that your collection lives in forty browser bookmarks that rot at maybe five per cent a year. That was exactly my situation, and Mealie fixed it completely.

Skip it if you cook from books. Photographing pages into a recipe manager is a data-entry project dressed up as a hobby, and the books are already better organised than the software.

Fourteen months in, I open it four or five times a week, my partner uses the shopping list without complaint, and I have restored it from backup once, successfully, after breaking Postgres myself in a manner too embarrassing to detail. That is a passing grade in this house.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.