Contents

Tandoor: When Your Recipe Collection Outgrows Browser Bookmarks

A serious self-hosted kitchen brain with meal plans and shopping lists

Contents

My recipe “system” used to be a browser folder with 240-odd bookmarks, a Notes app full of half-typed ingredient lists, and a recurring Sunday-evening ritual of squinting at my phone in the supermarket trying to remember whether I needed one tin of chickpeas or three. It worked, in the sense that a shopping trolley with a wonky wheel works. It got me there, mostly, with some swearing.

Tandoor is what I replaced all of that with, and a year in I’m not going back.

What Tandoor actually is

Advertisement

Tandoor Recipes is a self-hosted recipe manager, and the word “manager” is doing real work in that sentence. It is not a place to dump screenshots of recipes. It is a structured database of food: every recipe is broken into steps, every step into ingredients, and every ingredient into an amount, a unit and a food. Once your data lives in that shape, the software can do clever things with it.

The headline features, roughly in the order I came to love them:

  • Recipe import / scraping. Paste a URL and Tandoor pulls the recipe straight off the page, parsing the ingredients into its structured format. Most decent recipe sites publish machine-readable markup, and Tandoor reads it.
  • Ingredient parsing with units and scaling. Because amounts are real numbers with real units, you can ask for the same dish for two people or for eleven, and every quantity rescales.
  • Meal plans. A calendar you drag recipes onto.
  • Auto-generated shopping lists with supermarket aisle ordering, so the list is sorted the way you actually walk the shop.
  • Keywords and books for organising the pile once it gets large.

If you’ve used a lighter recipe app — something like Mealie, or one of the dozens of “save a link, see a card” tools — Tandoor will feel like the heavy-duty cousin. It does more. It also asks more of you, both in setup and in the discipline of structuring your recipes properly. That trade is the whole story here, and I’ll come back to it.

Why structure beats screenshots

The temptation, when your recipes are a mess, is to reach for the tidiest-looking tool: something that saves a link and shows you a pretty card. I did that for years. The trouble is that a card is just a picture of a recipe. You cannot ask a picture to double itself, or to tell you that three of this week’s dishes all want onions so you should buy a net rather than four loose ones. A screenshot is storage; a database is leverage.

Tandoor’s whole design bet is that if you pay the one-time cost of turning a recipe into structured data — steps, ingredients, amounts, units, foods — you unlock a pile of downstream behaviour for free forever after. That is the same bet every good tool makes: do the boring modelling once, reap the automation indefinitely. It’s the reason I put up with the setup, and it’s the reason I’d steer anyone who cooks the same six meals on repeat away from it, because they’ll pay the cost and never touch the leverage.

The unit of value here is the food object. In Tandoor, “garlic” is a thing that exists once, independent of the forty recipes that reference it. That indirection is what lets it merge shopping lists, track what you have in the pantry, and — if you go further than I have — flag allergens or nutrition. Lighter apps store “2 cloves garlic” as a string of text, which looks identical on screen and is useless the moment you want the software to reason about it.

Standing it up

Advertisement

Tandoor is a Django application, which in practice means a gunicorn app server, a Postgres database, and an nginx container serving the static and media files. Here’s a trimmed compose file close to what I run:

 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
services:
  db_recipes:
    image: postgres:15-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_DB=djangodb
      - POSTGRES_USER=tandoor
      - POSTGRES_PASSWORD=changeme_pg
    volumes:
      - ./postgresql:/var/lib/postgresql/data

  web_recipes:
    image: vabene1111/recipes
    restart: unless-stopped
    environment:
      - SECRET_KEY=please_generate_a_long_random_string
      - DB_ENGINE=django.db.backends.postgresql
      - POSTGRES_HOST=db_recipes
      - POSTGRES_DB=djangodb
      - POSTGRES_USER=tandoor
      - POSTGRES_PASSWORD=changeme_pg
    volumes:
      - ./staticfiles:/opt/recipes/staticfiles
      - ./mediafiles:/opt/recipes/mediafiles
    depends_on:
      - db_recipes

  nginx_recipes:
    image: nginx:mainline-alpine
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./staticfiles:/static:ro
      - ./mediafiles:/media:ro
    depends_on:
      - web_recipes

Two things bite people. First, generate a real SECRET_KEYopenssl rand -hex 32 does it — because the default-or-blank route ends in tears. Second, the static and media volumes must be shared between the app and nginx; the app writes them, nginx serves them, and if they don’t point at the same directory your images and stylesheets quietly vanish. Put the whole thing behind your usual reverse proxy for TLS and you’re done.

The actual workflow

Here’s a normal week for me.

Import or enter. I find a recipe online, paste the URL into Tandoor, and it scrapes it. The parse is good but not psychic, so I spend two minutes fixing the bits it got wrong — usually splitting a compound ingredient line, or correcting a unit. Recipes from a cookbook I type by hand once; thereafter they’re permanent.

Structure the ingredients. This is the bit lighter apps skip, and it’s where the payoff lives. Each ingredient gets an amount, a unit, and a food. “2 cloves garlic” becomes amount 2, unit clove, food garlic. Do this properly and everything downstream just works.

Scale. Cooking for guests, I bump the servings from 4 to 8 and watch every quantity double. No mental arithmetic, no wrong-by-a-factor disasters.

Plan the week. I open the meal plan and drag recipes onto days. Five minutes on a Saturday and the week is decided.

Push to a shopping list. One click turns the meal plan into a consolidated shopping list. Three recipes that each want onions become one line with the totals added up, and the whole list is sorted by supermarket aisle so I’m not backtracking past the tinned tomatoes for the fourth time.

That aisle ordering deserves a note, because it’s the feature I underrated on day one and now can’t live without. You define supermarkets and assign foods to aisles once. Thereafter every generated list is ordered to match how you physically walk your usual shop. It sounds trivial. It saves ten minutes and a great deal of trolley-reversing every single week, and it’s the sort of quiet compounding win that self-hosting a proper tool buys you over a bookmark folder.

Backups, because it’s now your data

The moment your recipes live in a Postgres database rather than a browser folder, you own an operational responsibility you didn’t have before: if that database dies, your recipes die with it. This is the boring side of self-hosting that nobody puts in the feature list, and it’s exactly the kind of load-bearing dependency I argue you should pay down first in my piece on home-lab technical debt. For Tandoor that means a nightly pg_dump alongside a copy of the media directory:

1
2
3
4
5
6
7
8
# nightly Tandoor backup: database + uploaded images
docker exec db_recipes pg_dump -U tandoor djangodb \
  | gzip > "/backups/tandoor-$(date +%F).sql.gz"

tar czf "/backups/tandoor-media-$(date +%F).tar.gz" ./mediafiles

# prune backups older than 30 days
find /backups -name 'tandoor-*' -mtime +30 -delete

Restore is the dump piped back into a fresh Postgres container and the media tarball unpacked into place. Test that restore at least once before you need it — an untested backup is a rumour.

Troubleshooting the bits that bite

Scraped ingredients come in as one mangled line. When Tandoor can’t parse a site cleanly, it dumps the whole ingredient into the “note” field with no amount or unit. The fix is manual but quick: edit the step, split the line, and set amount/unit/food properly. Do it once and it’s permanent. If a site consistently scrapes badly, it usually lacks proper recipe markup — there’s not much Tandoor can do about a page that doesn’t publish structured data.

Images and CSS vanish after an update. Nine times out of ten this is the shared-volume trap. The app container writes static and media files; the nginx container serves them; if they don’t mount the same host directories, nginx serves nothing. After any image bump, confirm both containers point at identical ./staticfiles and ./mediafiles paths, then re-run the container’s collectstatic if the app image changed.

“CSRF verification failed” on login behind a reverse proxy. Django is strict about which hostnames it trusts. If you front Tandoor with a proxy on mylab.local, set the ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS environment variables to include that hostname (with the scheme, e.g. https://recipes.mylab.local). Miss this and login silently fails with an opaque error.

A migration fails after a big version jump. Tandoor is under active development and occasionally ships schema changes that don’t like being skipped over. Update in steps rather than leaping across a year of releases, take a pg_dump first, and read the release notes for any manual migration step. This is standard Django-app hygiene, and it’s why the backup above isn’t optional.

The honest verdict

Tandoor is genuinely excellent, and it is also more software than a lot of people need.

It’s for you if you cook regularly from a growing collection, if meal-planning-to-shopping-list is a chore you’d happily automate, and if the structured-data discipline appeals rather than annoys. The aisle-ordered shopping list alone repaid the setup for me within a fortnight.

It’s overkill if you cook the same dozen things on rotation, or if you just want somewhere tidy to stash links. For that, a lighter manager — or honestly, a better-organised bookmark folder — will serve you fine, with none of the Postgres-and-nginx ceremony.

One honest caveat before you commit: the discipline is the real cost, not the RAM. Tandoor runs happily on modest hardware — a couple of hundred megabytes of memory, a small Postgres, nothing that should tempt you toward the home-lab upgrade trap of buying a bigger box to run a recipe app. The thing that will actually determine whether Tandoor works for you is whether you’ll keep structuring recipes properly when you’re tired and just want dinner sorted. If you will, it compounds beautifully; every recipe you enter makes the next meal plan faster. If you won’t, the structured data rots into half-filled forms and you’d have been happier with a card-based app that asks nothing of you.

For me the discipline stuck because the payoff is immediate and weekly. The scaling saves arithmetic every time I cook for guests; the aisle-sorted list saves ten minutes every shop; the meal plan removes the Saturday “what are we eating this week” negotiation. Those are small wins, but they land every single week, and small-but-weekly beats large-but-rare every time.

I sit firmly in the first camp. My 240 bookmarks are gone, the supermarket swearing has stopped, and the wonky trolley wheel is, sadly, the only part of the old system I couldn’t fix.

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.