feat: add Tandoor recipe manager to infrastructure

Added Tandoor Recipes as a comprehensive recipe management solution:

**Tandoor Stack** (tandoor.pivoine.art):
- Modern recipe manager with smart scaling and collaboration
- PostgreSQL backend for recipe persistence
- Email notifications via IONOS SMTP
- Static and media file storage in dedicated volumes
- User signups disabled (admin-only access)

**Features:**
- Smart recipe scaling (auto-adjust ingredients for servings)
- Spaces for collaboration (family/roommate recipe sharing)
- Meal planning and shopping lists
- Recipe import from URLs
- Mobile app support (Kitshn app)
- Nutritional information and pricing

**Infrastructure updates:**
- Added tandoor database to PostgreSQL init script
- Added environment variables to arty.yml
- Updated compose.yaml include list
- Added Tandoor volumes (staticfiles, mediafiles) to Restic backup
- Configured email notifications for invitations and notifications

**Tech stack:**
- Django/Python backend
- Vue.js frontend
- PostgreSQL database (shared core instance)
- Gunicorn WSGI server

Tandoor provides superior UX compared to Mealie with better recipe
scaling, collaboration features, and mobile app experience.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-08 20:54:48 +01:00
parent edbae4e932
commit af18e8273e
5 changed files with 107 additions and 1 deletions

View File

@@ -54,6 +54,19 @@ envs:
MATTERMOST_IMAGE: mattermost/mattermost-team-edition:latest
MATTERMOST_TRAEFIK_HOST: mattermost.pivoine.art
MATTERMOST_DB_NAME: mattermost
# Tandoor
TANDOOR_TRAEFIK_ENABLED: true
TANDOOR_COMPOSE_PROJECT_NAME: tandoor
TANDOOR_IMAGE: vabene1111/recipes:latest
TANDOOR_TRAEFIK_HOST: tandoor.pivoine.art
TANDOOR_DB_NAME: tandoor
TANDOOR_ENABLE_SIGNUP: 0
TANDOOR_REVERSE_PROXY_AUTH: 0
TANDOOR_EMAIL_USE_TLS: 0
TANDOOR_EMAIL_USE_SSL: 1
TANDOOR_GUNICORN_MEDIA: 0
TANDOOR_COMMENT_PREF_DEFAULT: 1
TANDOOR_SHOPPING_MIN_AUTOSYNC_INTERVAL: 5
# Scrapy
SCRAPY_TRAEFIK_ENABLED: true
SCRAPY_COMPOSE_PROJECT_NAME: scrapy

View File

@@ -4,6 +4,7 @@ include:
- awsm/compose.yaml
- sexy/compose.yaml
- mattermost/compose.yaml
- tandoor/compose.yaml
- scrapy/compose.yaml
- n8n/compose.yaml
- stash/compose.yaml

View File

@@ -33,6 +33,10 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
SELECT 'CREATE DATABASE mattermost'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mattermost')\gexec
-- Tandoor recipe manager database
SELECT 'CREATE DATABASE tandoor'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'tandoor')\gexec
-- Grant privileges to all databases
GRANT ALL PRIVILEGES ON DATABASE directus TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE umami TO $POSTGRES_USER;
@@ -40,11 +44,12 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
GRANT ALL PRIVILEGES ON DATABASE linkwarden TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE joplin TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE mattermost TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE tandoor TO $POSTGRES_USER;
-- Log success
SELECT 'Compose databases initialized:' AS status;
SELECT datname FROM pg_database
WHERE datname IN ('directus', 'umami', 'n8n', 'linkwarden', 'joplin', 'mattermost')
WHERE datname IN ('directus', 'umami', 'n8n', 'linkwarden', 'joplin', 'mattermost', 'tandoor')
ORDER BY datname;
EOSQL
@@ -59,4 +64,5 @@ echo " • n8n - Workflow automation database"
echo " • linkwarden - Bookmark manager database"
echo " • joplin - Note-taking server database"
echo " • mattermost - Chat platform database"
echo " • tandoor - Recipe manager database"
echo ""

View File

@@ -23,6 +23,8 @@ services:
- backup_mattermost_config:/volumes/mattermost_config:ro
- backup_mattermost_data:/volumes/mattermost_data:ro
- backup_mattermost_plugins:/volumes/mattermost_plugins:ro
- backup_tandoor_staticfiles:/volumes/tandoor_staticfiles:ro
- backup_tandoor_mediafiles:/volumes/tandoor_mediafiles:ro
- backup_scrapyd_data:/volumes/scrapyd_data:ro
- backup_scrapy_code:/volumes/scrapy_code:ro
- backup_n8n_data:/volumes/n8n_data:ro
@@ -95,6 +97,12 @@ volumes:
backup_mattermost_plugins:
name: mattermost_plugins
external: true
backup_tandoor_staticfiles:
name: tandoor_staticfiles
external: true
backup_tandoor_mediafiles:
name: tandoor_mediafiles
external: true
backup_scrapyd_data:
name: scrapy_scrapyd_data
external: true

78
tandoor/compose.yaml Normal file
View File

@@ -0,0 +1,78 @@
services:
tandoor:
image: ${TANDOOR_IMAGE:-vabene1111/recipes:latest}
container_name: ${TANDOOR_COMPOSE_PROJECT_NAME}_app
restart: unless-stopped
environment:
# Django settings
SECRET_KEY: ${TANDOOR_SECRET_KEY}
ALLOWED_HOSTS: ${TANDOOR_TRAEFIK_HOST}
TIMEZONE: ${TIMEZONE:-Europe/Berlin}
# Database configuration
DB_ENGINE: django.db.backends.postgresql
POSTGRES_HOST: ${CORE_DB_HOST}
POSTGRES_PORT: ${CORE_DB_PORT}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${TANDOOR_DB_NAME}
# Application settings
ENABLE_SIGNUP: ${TANDOOR_ENABLE_SIGNUP:-0}
REVERSE_PROXY_AUTH: ${TANDOOR_REVERSE_PROXY_AUTH:-0}
# Email configuration (IONOS SMTP)
EMAIL_HOST: ${EMAIL_SMTP_HOST}
EMAIL_PORT: ${EMAIL_SMTP_PORT}
EMAIL_HOST_USER: ${EMAIL_SMTP_USER}
EMAIL_HOST_PASSWORD: ${EMAIL_SMTP_PASSWORD}
EMAIL_USE_TLS: ${TANDOOR_EMAIL_USE_TLS:-0}
EMAIL_USE_SSL: ${TANDOOR_EMAIL_USE_SSL:-1}
DEFAULT_FROM_EMAIL: ${EMAIL_FROM}
# Gunicorn settings
GUNICORN_MEDIA: ${TANDOOR_GUNICORN_MEDIA:-0}
# Optional features
COMMENT_PREF_DEFAULT: ${TANDOOR_COMMENT_PREF_DEFAULT:-1}
SHOPPING_MIN_AUTOSYNC_INTERVAL: ${TANDOOR_SHOPPING_MIN_AUTOSYNC_INTERVAL:-5}
volumes:
- tandoor_staticfiles:/opt/recipes/staticfiles
- tandoor_mediafiles:/opt/recipes/mediafiles
depends_on:
- postgres
networks:
- compose_network
labels:
- 'traefik.enable=${TANDOOR_TRAEFIK_ENABLED}'
# HTTP to HTTPS redirect
- 'traefik.http.middlewares.${TANDOOR_COMPOSE_PROJECT_NAME}-redirect-web-secure.redirectscheme.scheme=https'
- 'traefik.http.routers.${TANDOOR_COMPOSE_PROJECT_NAME}-web.middlewares=${TANDOOR_COMPOSE_PROJECT_NAME}-redirect-web-secure'
- 'traefik.http.routers.${TANDOOR_COMPOSE_PROJECT_NAME}-web.rule=Host(`${TANDOOR_TRAEFIK_HOST}`)'
- 'traefik.http.routers.${TANDOOR_COMPOSE_PROJECT_NAME}-web.entrypoints=web'
# HTTPS router
- 'traefik.http.routers.${TANDOOR_COMPOSE_PROJECT_NAME}-web-secure.rule=Host(`${TANDOOR_TRAEFIK_HOST}`)'
- 'traefik.http.routers.${TANDOOR_COMPOSE_PROJECT_NAME}-web-secure.tls.certresolver=resolver'
- 'traefik.http.routers.${TANDOOR_COMPOSE_PROJECT_NAME}-web-secure.entrypoints=web-secure'
- 'traefik.http.middlewares.${TANDOOR_COMPOSE_PROJECT_NAME}-web-secure-compress.compress=true'
- 'traefik.http.routers.${TANDOOR_COMPOSE_PROJECT_NAME}-web-secure.middlewares=${TANDOOR_COMPOSE_PROJECT_NAME}-web-secure-compress,security-headers@file'
# Service
- 'traefik.http.services.${TANDOOR_COMPOSE_PROJECT_NAME}-web-secure.loadbalancer.server.port=8080'
- 'traefik.docker.network=${NETWORK_NAME}'
# Watchtower
- 'com.centurylinklabs.watchtower.enable=${WATCHTOWER_LABEL_ENABLE}'
volumes:
tandoor_staticfiles:
name: ${TANDOOR_COMPOSE_PROJECT_NAME}_staticfiles
tandoor_mediafiles:
name: ${TANDOOR_COMPOSE_PROJECT_NAME}_mediafiles
networks:
compose_network:
name: ${NETWORK_NAME}
external: true