Self-hosting Tandoor Recipes with Docker Compose
A recipe box and meal planner that lives on your own disk. No calorie math, no subscription, no ads before the ingredient list.
What Tandoor is
Tandoor Recipes is a Django app for storing, organizing, and planning recipes. It scrapes a recipe from a URL (most food-blog markup parses cleanly), lets you tag and shelve it into “books,” schedules meals on a calendar, and rolls the week’s plan into a shopping list you can check off from a phone. Multi-user with permission levels, so it works for a household, not just one person.
It is not a calorie tracker and doesn’t try to be. There’s no daily log, no macro target, no streak. It’s closer to a Rolodex than a diary — the thing that answers “what did we do with that lamb shoulder in March” three years from now, instead of a shoebox of screenshots.
The project is actively maintained, self-hostable by design (there is no hosted SaaS version to upsell you into), and exports everything as structured data — recipes, meal plans, shopping lists — so nothing you type into it is stuck there.
Why it earns a place on the same box
If you’re already running a homelab for other reasons, Tandoor is a light addition: one Django container, one Postgres container, fronted by whatever reverse proxy you already run. No GPU, no message queue, no third service to keep patched. The heaviest resource draw is Postgres itself, and that’s already true of everything else on the stack.
The bigger reason is ownership. Recipe sites rotate their front-end, bury the method under a life story, and can vanish; a URL you scraped two years ago is not a recipe you still have. Tandoor’s import keeps a local, searchable copy the moment you save it.
docker-compose.yml
Tandoor’s own repository ships a working docker-compose.yml and .env.template under its docker/ directory — pull those directly rather than retyping them, since env var names have shifted across releases and the maintained copy is the one that matches your version. The shape looks like this:
services:
db_recipes:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=tandoor
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- POSTGRES_DB=tandoor
volumes:
- tandoor-db:/var/lib/postgresql/data
secrets:
- db_password
web_recipes:
image: ghcr.io/tandoorrecipes/recipes:latest
restart: unless-stopped
env_file: .env
depends_on:
- db_recipes
volumes:
- tandoor-static:/opt/recipes/staticfiles
- tandoor-media:/opt/recipes/mediafiles
expose:
- "8080"
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
tandoor-db:
tandoor-static:
tandoor-media:
Set SECRET_KEY and the Postgres credentials in .env before first boot, then:
docker compose up -d
docker compose exec web_recipes python manage.py migrate
First boot also prompts you to create an admin account on /accounts/signup/ — do this immediately and then turn signup off in the admin settings if you’re the only household on the instance.
Putting it behind a reverse proxy
Tandoor serves its own static and media files through the app container, so a reverse proxy just needs to forward everything and get out of the way — no special path-splitting like an app with a separate API host. If you’re already running Caddy for the rest of the homelab, add one more site block using the same pattern in Caddy reverse proxy for LAN-only access; if you’re on nginx instead, the certbot + nginx guide covers the TLS side.
Backups
The data that matters is the Postgres database plus the mediafiles volume (recipe images and any PDFs you’ve attached). Both are exactly the kind of thing the restore drill in Backups and restore for self-hosted nutrition data already covers — add a pg_dump of the tandoor database and the tandoor-media volume to the same Borgmatic or restic job you’re running for the rest of the stack rather than standing up a separate backup path for one more app.
Watch-outs
- Recipe scraping is best-effort. Sites that hide ingredients behind JavaScript rendering or unusual schema markup sometimes need manual entry. Tandoor’s browser bookmarklet helps when the server-side scrape comes back empty.
- Media storage grows. Every recipe photo lives in the
mediafilesvolume. It’s not large compared to the OFF mirror or a Postgres dump, but it’s worth including in whatever disk-usage check you already run. - Run migrations after every upgrade, not just the first boot — Django apps generally don’t self-migrate on container restart, and Tandoor is no exception.
References
- Tandoor Recipes documentation: docs.tandoor.dev
- Tandoor Recipes source and Docker install files: github.com/TandoorRecipes/recipes