Open-source alternatives guide
Self-Host Tandoor Recipes for Meal Planning 2026
Self-host Tandoor Recipes for recipe management with nutrition data in 2026. MIT license, ~5K stars, Python/Django — barcode scanning, shopping lists, meal.
TL;DR
Tandoor Recipes (MIT, ~5K GitHub stars, Python/Django) is a feature-rich self-hosted recipe manager with detailed nutritional information. While Mealie focuses on simplicity, Tandoor goes deeper: built-in nutritional database (OpenFoodFacts integration), barcode scanning for ingredients, detailed shopping list with supermarket aisle mapping, and full-text recipe search. If you care about nutrition tracking alongside recipes, Tandoor is the better choice.
Key Takeaways
- Tandoor: MIT, ~5K stars, Python/Django — recipes + nutrition + shopping + barcode scan
- OpenFoodFacts: Integration with the open food database for nutritional info
- Barcode scanning: Scan ingredient barcodes to add to shopping lists
- Supermarket mapping: Map ingredients to your store's aisles for efficient shopping
- Meal planning: Weekly planner with nutritional summary
- Full-text search: Search inside recipes (not just titles)
Tandoor vs Mealie
| Feature | Tandoor | Mealie |
|---|---|---|
| License | MIT | AGPL 3.0 |
| GitHub Stars | ~5K | ~7K |
| UI Complexity | Higher | Simpler |
| Nutrition data | Yes (OpenFoodFacts) | Limited |
| Barcode scan | Yes | No |
| Shopping list | Yes (advanced) | Yes (basic) |
| Supermarket mapping | Yes | No |
| Recipe scraping | Yes | Yes (better) |
| Meal planning | Yes | Yes |
| OCR import | Yes | Yes |
| API | Yes | Yes |
Part 1: Docker Setup
# docker-compose.yml
services:
db_recipes:
restart: always
image: postgres:16-alpine
volumes:
- postgresql_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_USER: djangodb
POSTGRES_DB: djangodb
web_recipes:
image: vabene1111/recipes:latest
container_name: tandoor
restart: always
ports:
- "8080:8080"
volumes:
- staticfiles:/opt/recipes/staticfiles
- mediafiles:/opt/recipes/mediafiles
depends_on:
- db_recipes
environment:
SECRET_KEY: "${SECRET_KEY}" # openssl rand -hex 64
DB_ENGINE: django.db.backends.postgresql
POSTGRES_HOST: db_recipes
POSTGRES_PORT: "5432"
POSTGRES_USER: djangodb
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_DB: djangodb
ALLOWED_HOSTS: "meals.yourdomain.com"
GUNICORN_MEDIA: "0"
TIMEZONE: "America/Los_Angeles"
ACCOUNT_EMAIL_SUBJECT_PREFIX: "[Tandoor]"
DEBUG: "0"
nginx_recipes:
image: nginx:mainline-alpine
restart: always
ports:
- "80:80"
volumes:
- staticfiles:/static:ro
- mediafiles:/media:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
- web_recipes
volumes:
postgresql_data:
staticfiles:
mediafiles:
# nginx/conf.d/recipes.conf
server {
listen 80;
server_name _;
client_max_body_size 16M;
location /static/ {
alias /static/;
}
location /media/ {
alias /media/;
}
location / {
proxy_pass http://web_recipes:8080;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Generate secret key:
openssl rand -hex 64
docker compose up -d
Part 2: HTTPS with Caddy
Replace the nginx container with Caddy for automatic HTTPS:
meals.yourdomain.com {
handle /static/* {
file_server {
root /path/to/staticfiles
}
}
handle /media/* {
file_server {
root /path/to/mediafiles
}
}
reverse_proxy web_recipes:8080
}
Or simply proxy through Caddy to the nginx container:
meals.yourdomain.com {
reverse_proxy localhost:80
}
Part 3: Initial Setup
- Visit
https://meals.yourdomain.com - Create admin account
- Admin Panel → Household → Create Your Household
- Invite other users to the household
Part 4: Import Recipes
From URL
- Recipes → + New → Import From URL
- Paste recipe URL
- Tandoor extracts ingredients, instructions, images
- Review and save
Manual Entry
- Recipes → + New → Manual
- Add ingredients (search by name → Tandoor suggests from food database)
- Add steps with photos
- Set servings, time, difficulty
From OCR (Photo)
- Recipes → + → Create From Image
- Upload photo of printed/handwritten recipe
- Tandoor uses OCR to extract text
- Review and correct
Part 5: Nutrition Information
When you add ingredients, Tandoor looks up nutrition data:
- Type ingredient name → Tandoor suggests matches from OpenFoodFacts
- Select the correct product → nutrition data auto-filled
- Recipe shows: calories, protein, carbs, fat per serving
Manual Nutrition Lookup
- Foods → Search or Create Food
- Type name or scan barcode
- Enter or import nutrition data per 100g
Part 6: Shopping Lists
Generate shopping lists from recipes:
- Add to Shopping List from any recipe (adjust servings)
- Multiple recipes → shopping list auto-combines quantities
- Shopping → Lists → My List
Supermarket Mapping
Map ingredients to your supermarket's sections:
- Supermarkets → Create Supermarket
- Add sections: Produce, Dairy, Meat, Canned Goods, Frozen, Bakery
- Foods → Edit Food → assign to supermarket section
Shopping list sorts by supermarket section → efficient shopping path through the store.
Barcode Scanning
- Shopping → Scan Barcode
- Scan product barcode with your phone camera
- Tandoor looks up the product → adds to shopping list
Part 7: Meal Planning
- Meal Plan → Weekly View
- Click any day → Add Recipe
- Set servings (for the family)
- Nutritional summary shown for the week
- Generate Shopping List from the meal plan
Part 8: Recipe Scraping Sites
Tandoor supports 700+ recipe websites including:
- allrecipes.com
- seriouseats.com
- food52.com
- BBC Good Food
- NYT Cooking
- Bon Appétit
- Epicurious
Part 9: API
# Get API token from Settings → API → Create Token
# List recipes:
curl https://meals.yourdomain.com/api/recipe/ \
-H "Authorization: Token YOUR_API_TOKEN"
# Search recipes:
curl "https://meals.yourdomain.com/api/recipe/?query=pasta" \
-H "Authorization: Token YOUR_API_TOKEN"
# Create a shopping list:
curl -X POST https://meals.yourdomain.com/api/shopping-list/ \
-H "Authorization: Token YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"recipes": [{"recipe_id": 42, "servings": 4}]}'
Maintenance
# Update Tandoor:
docker compose pull
docker compose up -d
# Run migrations:
docker exec tandoor python manage.py migrate
# Backup:
# Database:
docker exec db_recipes pg_dump -U djangodb djangodb | gzip \
> tandoor-db-$(date +%Y%m%d).sql.gz
# Media files (uploaded photos):
tar -czf tandoor-media-$(date +%Y%m%d).tar.gz \
$(docker volume inspect tandoor_mediafiles --format '{{.Mountpoint}}')
# Logs:
docker compose logs -f web_recipes
Why Self-Host Tandoor Recipes
There's no dominant SaaS recipe manager that charges a meaningful monthly fee — the market is fragmented between apps like Paprika ($29.99 one-time per platform), Whisk (ad-supported), and Yummly (acquired by Whirlpool). The case for self-hosting Tandoor isn't primarily about cost savings; it's about features and data longevity.
Paprika is the closest commercial equivalent to Tandoor, and it's genuinely a good app. But it doesn't integrate with OpenFoodFacts for nutritional data, has no barcode scanning, and its supermarket mapping feature is more limited than Tandoor's. If nutrition tracking is part of your cooking workflow — and for anyone managing dietary restrictions, macros, or caloric intake, it is — Tandoor is the most complete open source option available. The integration with OpenFoodFacts gives you nutrition data for hundreds of thousands of products that you can attach to ingredients and see totals at the recipe and meal plan level.
Data longevity is the other argument. Recipe apps have a track record of shutting down or pivoting. Pepperplate shut down in 2019. BigOven went freemium and locked off features. When a recipe app disappears, your recipe collection disappears with it. Tandoor stores everything in a PostgreSQL database you control. Export at any time, restore anywhere. If you've spent years clipping recipes, building shopping lists, and tagging meals by dietary profile, that data has real value and deserves durable storage.
The household-sharing model is another differentiator. Tandoor supports multiple users within a shared household. One person builds the weekly meal plan, another generates the shopping list, and the kitchen app shows the recipe to whoever is cooking. No per-seat pricing, no premium tier for family features.
When NOT to self-host Tandoor. The UI has more complexity than simpler alternatives like Mealie. If your goal is "import a recipe URL and be done," Mealie's cleaner interface may suit you better. Tandoor's strength is in the nutrition and shopping features — if you don't need those, you're running extra complexity for nothing. Also, the three-service setup (PostgreSQL, Django app, Nginx) requires a bit more maintenance than a single-container alternative.
Prerequisites
Tandoor runs as three services: a PostgreSQL database, a Django application server (gunicorn), and an Nginx container serving static files and media. Together they consume around 300–400MB of RAM at idle. A Hetzner CX22 (2 vCPU, 4GB RAM) at €4.50/month is more than adequate. If you're consolidating multiple services on one VPS, the CX32 (8GB RAM) at €9.90/month gives you comfortable headroom. See the VPS comparison guide for full provider comparisons.
Docker Engine 24+ and Docker Compose v2 are required. The compose configuration includes three services — plan to review the Nginx configuration in nginx/conf.d/recipes.conf to ensure client_max_body_size is set high enough for recipe image uploads (16MB is the default; increase to 50MB if you import many high-resolution images).
DNS: create an A record for meals.yourdomain.com (or your preferred subdomain) pointing to your VPS IP. If you replace Nginx with Caddy for HTTPS termination, Caddy handles certificate provisioning automatically. Port 80 and 443 must be open.
Generate the SECRET_KEY before first launch with openssl rand -hex 64. This key signs sessions and tokens — losing it after first use invalidates all sessions but doesn't lose data.
Production Security Hardening
Tandoor stores your household's recipe collection, meal history, and optionally nutritional data. It's not high-risk data, but the server itself is a general-purpose Linux host that needs hardening.
UFW firewall. Block all ports except what's needed:
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Internal ports (Django's 8080, PostgreSQL's 5432) should never be exposed publicly.
Fail2ban. Protect the login endpoint:
apt install fail2ban -y
Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
systemctl restart fail2ban
Secrets in .env. The SECRET_KEY and POSTGRES_PASSWORD are stored in your .env file. Keep it readable only by root:
chmod 600 .env
Never commit .env to version control.
SSH hardening. Use key-based authentication:
# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no
systemctl restart sshd
Automatic updates. Install unattended-upgrades for automatic OS security patches:
apt install unattended-upgrades -y
dpkg-reconfigure --priority=low unattended-upgrades
Database backups. Your recipe collection lives in PostgreSQL. Back it up daily — the automated backup guide covers Restic-based encrypted backups to S3-compatible storage, which works well for PostgreSQL dump files. Also back up the mediafiles volume if you've uploaded recipe photos. For a complete security reference, see the self-hosting security checklist.
Troubleshooting Common Issues
Django app fails to start with "ALLOWED_HOSTS" error. The ALLOWED_HOSTS environment variable must exactly match your domain (e.g., meals.yourdomain.com). If it doesn't match the Host header Nginx passes to Django, you'll get a 400 Bad Request or a Django SuspiciousOperation error. Check your .env and restart the web_recipes container.
Static files not loading (CSS/JS broken after deployment). Tandoor separates static file serving to Nginx. If the staticfiles volume isn't populated, CSS won't load. Run the collectstatic command manually: docker exec tandoor python manage.py collectstatic --noinput. This is usually a one-time step that happens automatically on first start, but can fail silently if there's a permissions issue on the volume.
Recipe import from URL fails. Tandoor uses recipe-scrapers to extract structured data from recipe websites. If a URL fails, check whether the site is on the supported list (700+ sites). Unsupported sites can sometimes be imported with the browser extension or manually. If a previously working site stops importing, it may have changed its HTML structure — file an issue on the Tandoor GitHub with the failing URL.
PostgreSQL "connection refused" on startup. The web_recipes service uses depends_on: db_recipes, but Docker's health checking doesn't wait for PostgreSQL to be fully initialized — just for the container to start. On slow hosts, the database may not be ready by the time Django tries to connect. Add a restart: always to web_recipes in your compose file so it retries automatically, or add a health check to the db_recipes service.
Migrations fail after Tandoor update. Always run migrations after pulling a new Tandoor image: docker exec tandoor python manage.py migrate. If migrations fail due to a database conflict, check the Tandoor changelog for breaking schema changes. Restore from a pre-update backup if needed.
Media uploads (recipe photos) not persisting. Verify the mediafiles Docker volume is correctly mounted at /opt/recipes/mediafiles in the Django container and at /media in the Nginx container. If they're not pointing to the same volume, uploaded images are accessible immediately but disappear after a container restart.
Nutritional data not appearing for ingredients. When you type an ingredient name, Tandoor queries OpenFoodFacts for matching products. If no results appear, it may be that the specific product isn't in the OpenFoodFacts database (it's community-maintained and more comprehensive for packaged foods than fresh produce), or there may be a connectivity issue reaching the OpenFoodFacts API. You can add nutritional data manually by going to Foods → Create Food and entering per-100g values directly.
Supermarket layout not appearing in shopping list. Supermarket mapping requires: creating a supermarket, adding categories to it, and then assigning each food item to a category. The category assignment is the step most people miss. Go to Foods → search for an ingredient → Edit → assign it to a supermarket category. Once foods are mapped, the shopping list sorts by your store's layout automatically.
Recipe scraping fails on a supported site. Recipe websites frequently update their HTML structure, which breaks scrapers. If a site that should be supported returns an empty import, check the Tandoor GitHub issues to see if others have reported the same site. The recipe-scrapers library that Tandoor uses is community-maintained and usually has a fix within days of a site update. Update Tandoor to the latest version and retry — docker compose pull && docker compose up -d is often all that's needed.
Meal plan shows incorrect calorie totals. Calorie calculations depend on the nutritional data attached to each food item in your library. If a food item was added without nutritional data, its contribution to the meal plan total is zero. Review the foods used in your planned recipes under Foods → check for any showing N/A for calories, and either link them to OpenFoodFacts entries or manually enter per-100g values.
See the best open source alternatives to Notion for additional self-hosted knowledge management tools that pair well with a recipe database.
See all open source food and lifestyle tools at OSSAlt.com/categories/lifestyle.
The SaaS-to-Self-Hosted Migration Guide (Free PDF)
Step-by-step: infrastructure setup, data migration, backups, and security for 15+ common SaaS replacements. Used by 300+ developers.
Join 300+ self-hosters. Unsubscribe in one click.