From af18e8273e6520a8543489957c3497ee85341c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 8 Nov 2025 20:54:48 +0100 Subject: [PATCH] feat: add Tandoor recipe manager to infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- arty.yml | 13 +++++ compose.yaml | 1 + core/postgres/init/01-init-databases.sh | 8 ++- restic/compose.yaml | 8 +++ tandoor/compose.yaml | 78 +++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tandoor/compose.yaml diff --git a/arty.yml b/arty.yml index ac95027..2c3fffb 100644 --- a/arty.yml +++ b/arty.yml @@ -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 diff --git a/compose.yaml b/compose.yaml index 1821dc4..b649bb7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -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 diff --git a/core/postgres/init/01-init-databases.sh b/core/postgres/init/01-init-databases.sh index a7270d4..cc7c8cb 100644 --- a/core/postgres/init/01-init-databases.sh +++ b/core/postgres/init/01-init-databases.sh @@ -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 "" diff --git a/restic/compose.yaml b/restic/compose.yaml index 51edcb8..fa31382 100644 --- a/restic/compose.yaml +++ b/restic/compose.yaml @@ -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 diff --git a/tandoor/compose.yaml b/tandoor/compose.yaml new file mode 100644 index 0000000..c751e09 --- /dev/null +++ b/tandoor/compose.yaml @@ -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