Teams and home users who want to consolidate recipes, plan weekly meals, and generate shopping lists without feeding their data to third-party SaaS platforms need a self-hosted alternative to commercial recipe services. Tandoor Recipes is a Django-based recipe management application that covers the full cooking workflow: import recipes from any URL with structured metadata extraction, organize them by category and tag, plan meals on a weekly calendar, and generate shopping lists automatically from selected recipes. It supports multiple households through its Spaces feature, tracks nutritional data per dish, and integrates with barcode scanning on mobile. Unlike lightweight single-file tools, Tandoor uses PostgreSQL for structured relational storage, making it suitable for large recipe collections with thousands of entries, multiple users, and production workloads that need reliable full-text search and data integrity guarantees.
This guide deploys a production-ready Tandoor instance on Ubuntu 22.04 LTS using Docker Compose for container lifecycle management, PostgreSQL 15 for structured recipe storage, and Caddy for automatic HTTPS. By the end you will have a publicly accessible Tandoor installation with TLS, persistent volumes for the database and media uploads, environment-based secret handling, and a superuser account ready for household or small-team use.
Architecture and flow overview
Tandoor runs as a Django/Gunicorn application in a single container backed by a PostgreSQL 15 database in a separate container. The official image bundles both Gunicorn and Nginx: Gunicorn handles Python application requests while Nginx serves static assets directly, reducing latency for CSS, JavaScript, and cached recipe images. Recipe images and other user-uploaded media are stored on a named Docker volume, decoupling them from the application container so upgrades never lose user-uploaded content. A third named volume handles static file collection.
Traffic flow: a browser connects to Caddy on port 443. Caddy terminates TLS using an automatically provisioned Let's Encrypt certificate and reverse-proxies all requests to the Tandoor container listening on localhost port 8080. Caddy's automatic HTTP-to-HTTPS redirect and HSTS headers are active by default with no extra configuration. The Docker Compose stack binds the Tandoor container exclusively to the loopback interface (127.0.0.1:8080), ensuring no traffic reaches the application directly from the public internet. PostgreSQL is not exposed on any host port; Tandoor connects to it over the internal Docker bridge network using the service name db. Both named volumes survive container image updates and host reboots without manual intervention.
Prerequisites
- Ubuntu 22.04 LTS server with root or sudo access
- Minimum 1 GB RAM and 1 vCPU (Tandoor uses approximately 200–300 MB under normal load)
- Docker Engine 24+ and Docker Compose Plugin installed
- Caddy 2.7+ installed as a systemd service
- A domain name (e.g.,
recipes.yourdomain.com) with an A record pointing to your server's public IP, fully propagated before starting Caddy - UFW or equivalent firewall with ports 80 and 443 open; all other inbound ports closed
Step-by-step deployment
1. Create the project directory and environment file
Create a dedicated directory for the Tandoor stack and write the environment configuration:
sudo mkdir -p /opt/tandoor
sudo chown $USER:$USER /opt/tandoor
cd /opt/tandoorCreate the environment file with your database credentials and secret key:
cat > /opt/tandoor/.env << 'EOF'
POSTGRES_HOST=db
POSTGRES_PORT=5432
POSTGRES_USER=tandoor
POSTGRES_PASSWORD=change_this_to_a_strong_password
POSTGRES_DB=tandoor_db
SECRET_KEY=change_this_to_a_64_char_random_string
DB_ENGINE=django.db.backends.postgresql
TANDOOR_PORT=8080
ALLOWED_HOSTS=*
GUNICORN_WORKERS=3
ENABLE_SIGNUP=0
TIMEZONE=UTC
EOF
chmod 600 /opt/tandoor/.env2. Write the Docker Compose file
Create /opt/tandoor/docker-compose.yml:
version: '3.9'
services:
db:
image: postgres:15-alpine
restart: unless-stopped
volumes:
- tandoor-db:/var/lib/postgresql/data
env_file: .env
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
web:
image: vabene1111/recipes:latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
env_file: .env
volumes:
- tandoor-media:/opt/recipes/mediafiles
- tandoor-static:/opt/recipes/staticfiles
depends_on:
db:
condition: service_healthy
volumes:
tandoor-db:
tandoor-media:
tandoor-static:3. Configure Caddy
Add a site block to /etc/caddy/Caddyfile:
recipes.yourdomain.com {
reverse_proxy 127.0.0.1:8080
}4. Start the stack and reload Caddy
cd /opt/tandoor
docker compose up -d
# Wait ~30 seconds for database migrations to complete
sleep 30
sudo systemctl reload caddy5. Create the admin account
cd /opt/tandoor
docker compose exec web python manage.py createsuperuserConfiguration and secrets handling
SECRET_KEY must be a cryptographically random string of at least 50 characters. Generate one before first launch:
python3 -c "import secrets; print(secrets.token_urlsafe(48))"POSTGRES_PASSWORD should be similarly random. Both secrets live only in /opt/tandoor/.env with mode 600, and are never committed to version control. ENABLE_SIGNUP=0 disables open self-registration — invite new users by creating accounts manually via the Django admin panel at /admin/. GUNICORN_WORKERS=3 handles typical household or small-team concurrency; increase to 5–7 for larger deployments or heavy recipe-import workloads.
To enable email notifications and shareable recipe links, add SMTP settings to .env:
EMAIL_HOST=smtp.yourprovider.com
EMAIL_PORT=587
[email protected]
EMAIL_HOST_PASSWORD=yoursmtppassword
EMAIL_USE_TLS=True
[email protected]Restart the web container after any .env change: docker compose up -d --force-recreate web.
Verification
Confirm both containers are running and healthy:
cd /opt/tandoor
docker compose psBoth db and web should show status Up (db as healthy). Verify Caddy is serving TLS:
curl -sI https://recipes.yourdomain.com | head -5Expect HTTP/2 200. Navigate to https://recipes.yourdomain.com/admin/ and log in with your superuser credentials to confirm database connectivity. Import a recipe from a public URL via Recipes > Add Recipe > Import from URL to verify end-to-end functionality including media download and structured data extraction.
Common issues and fixes
Web container exits immediately after startup — Usually a database connection failure before PostgreSQL finishes initializing. The healthcheck dependency should prevent this. If it occurs, run docker compose logs db to inspect PostgreSQL errors, then docker compose restart web once db shows healthy.
Static files (CSS/JS) returning 404 — The official image builds static files into the container on startup via collectstatic, but if you see missing assets, run docker compose exec web python manage.py collectstatic --noinput and restart the web service.
Recipe import fails for certain sites — Some publishers actively block automated fetches. Tandoor uses the recipe-scrapers library which supports 500+ sites. For unsupported sites, use the bookmarklet (available in Settings) or the manual entry form. Check docker compose logs web for detailed import error messages including the specific scraper exception.
Caddy returns 502 Bad Gateway — The Tandoor container is not listening on port 8080. Verify the port binding is 127.0.0.1:8080:8080 in docker-compose.yml and that the web service is running: docker compose ps and docker compose logs web.
PostgreSQL password authentication failed — The POSTGRES_PASSWORD value in .env must be identical for both the db and web services, and must match what was used when the volume was first initialized. If you change the password after the volume exists, drop the volume and reinitialize: docker compose down -v && docker compose up -d. This deletes all recipes — back up first.
FAQ
Can I import recipes from any website?
Tandoor uses the recipe-scrapers Python library which supports over 500 popular recipe sites including AllRecipes, Serious Eats, NYT Cooking, BBC Good Food, and Epicurious. For unsupported sites, use the browser bookmarklet to extract content from any page, or enter recipes manually using the structured editor with ingredient parser. The bookmarklet is available in your Tandoor user profile under Settings.
How do I back up my recipes and media?
Two components need regular backup: the PostgreSQL database and the media volume. For PostgreSQL: docker compose exec db pg_dump -U tandoor tandoor_db > /backup/tandoor_$(date +%Y%m%d).sql. For media uploads, copy the named volume directory from /var/lib/docker/volumes/tandoor_tandoor-media/_data/. Automate both with a daily cron job and ship backups to object storage or a secondary server. Test restores periodically by importing a dump into a staging instance.
Can multiple users share the same Tandoor instance?
Yes. Tandoor's Spaces feature allows multiple households or teams to share a single server while keeping their recipe collections private. Each Space has independent recipes, shopping lists, and meal plans. Create additional user accounts via the Django admin at /admin/ and assign them to their Space. Users can also be granted cross-space viewer access if a household wants shared meal planning and shopping list collaboration.
How do meal planning and shopping list generation work?
Meal planning is available from the Meal Planner tab in the main navigation. Drag recipes onto calendar days for the desired week. When your plan is ready, click Generate Shopping List to automatically combine and deduplicate ingredients across all planned meals. The shopping list groups items by food category and lets you check off items while shopping. You can share a live shopping list link with family members or export it to PDF.
Can I run Tandoor on a private LAN without a public domain?
Yes. For LAN-only use, change the Tandoor port binding to 0.0.0.0:8080:8080 and access the instance via your server's local IP address over plain HTTP. For HTTPS on a private network, use Caddy's tls internal directive to generate a locally trusted certificate, then add the Caddy root CA to your browser's trusted certificate store. This gives you HTTPS without Let's Encrypt and without a public domain.
How do I upgrade Tandoor to a newer version?
Pull the updated image and recreate the container. Named volumes are preserved across the operation so no data is lost:
cd /opt/tandoor
docker compose pull
docker compose up -d --force-recreateReview the GitHub releases page for migration notes or schema changes before upgrading between major versions. Django migrations run automatically on container start.
Is there a mobile app for Tandoor?
Yes. Tandoor has official companion apps for Android and iOS. Configure the app with your server URL and an API token generated in your Tandoor user profile under Settings > API Tokens. The mobile app supports offline recipe viewing for saved recipes, barcode scanning for ingredient lookup, and quick-add to the active shopping list. It uses the same Spaces you set up on the server, so all data stays synchronized.
Internal links
- Production Guide: Deploy Nextcloud with Docker Compose + Caddy + PostgreSQL — pair your recipe manager with a full file sync solution on the same server for storing meal photos and grocery receipts.
- Production Guide: Deploy Mealie with Docker Compose + Caddy + PostgreSQL — an alternative self-hosted recipe manager with a different interface approach if you prefer a card-first layout over Tandoor's planner-centric design.
- Production Guide: Deploy Wallos with Docker Compose + Caddy — track subscription costs for your self-hosted services alongside your recipe infrastructure on the same Ubuntu host.
Talk to us
If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.