MAATRIX / Блог / Tandoor Recipes на сервере: частые ошибки и решения

Tandoor Recipes на сервере: частые ошибки и решения

MAATRIX

Tandoor Recipes ставят на свой сервер обычно после того, как надоедает хранить рецепты в заметках или закладках браузера: тут и парсер рецептов с любого сайта, и учёт КБЖУ, и книги рецептов, и общий доступ для семьи. Но сразу после docker compose up начинаются вопросы — контейнер падает, страница отдаёт 400, при входе через домен вылетает CSRF, картинки не грузятся. Разберём, откуда растут ноги у этих ошибок и как их закрыть без пересборки с нуля.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Как устроен Tandoor в докере и где обычно путаются

Официальный docker-compose Tandoor — это набор из нескольких контейнеров, а не один монолит, и большинство проблем возникает именно на стыке между ними:

  • db — PostgreSQL, хранит рецепты, пользователей, планы питания;
  • web_recipes (или recipes) — сам Django-бэкенд на gunicorn, отдаёт API и обрабатывает логику;
  • nginx_recipes — раздаёт статику и медиа-файлы, проксирует запросы на бэкенд.

Состав сервисов и их имена немного отличаются между версиями compose-файла, поэтому если у вас другие названия контейнеров — ориентируйтесь на docker compose ps в своей директории, а не на имена из этой статьи буквально. Ключевая точка конфигурации — файл .env рядом с docker-compose.yml. В нём задаются:

POSTGRES_PASSWORD=...
SECRET_KEY=...
ALLOWED_HOSTS=recipes.example.com
DB_ENGINE=django.db.backends.postgresql
POSTGRES_HOST=db_recipes

Если этот файл собран наспех — скопирован пример без правок или потерян при переносе — почти все ошибки ниже вытекают именно отсюда. Первое, что стоит сделать при любой проблеме: docker compose logs -f web_recipes и docker compose logs -f db_recipes — почти всегда причина видна в первых 20 строках.

Backend не стартует: "connection refused" к базе

Классика первого запуска: контейнер с БД ещё поднимается (инициализирует volume, накатывает initdb), а backend уже пытается подключиться и валится с ошибкой вида django.db.utils.OperationalError: could not connect to server: Connection refused.

Причины обычно две:

  1. Гонка при старте. depends_on в docker-compose гарантирует только порядок запуска контейнеров, но не готовность Postgres принимать соединения — база может стартовать на 5-10 секунд дольше, чем нужно бэкенду для первой попытки подключения.
  2. Неверный POSTGRES_HOST. В .env указано localhost или 127.0.0.1 вместо имени сервиса в docker-сети (db_recipes, db — смотрите точное имя в вашем compose-файле).

Решение для первого случая — добавить healthcheck на db и condition: service_healthy в зависимости backend:

db_recipes:
  image: postgres:16
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U tandoor"]
    interval: 5s
    timeout: 5s
    retries: 10

web_recipes:
  depends_on:
    db_recipes:
      condition: service_healthy

Для второго — проверьте, что POSTGRES_HOST в .env совпадает с именем сервиса базы данных из docker-compose.yml, а не с адресом, который вы бы использовали для подключения снаружи. Если проблема плавающая (иногда стартует, иногда нет) — почти наверняка это гонка, а не опечатка в имени хоста.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

400 Bad Request и 403 CSRF при заходе через домен

Открывается сервер по IP — работает. Настроили домен и reverse-proxy (nginx или Traefik) — получаете Bad Request (400) или ошибку без описания. Причина в Django: домен, с которого пришёл запрос, не совпадает ни с одним значением в ALLOWED_HOSTS.

# .env
ALLOWED_HOSTS=recipes.example.com,www.recipes.example.com

После правки — обязательно пересоздать контейнер, а не просто перезапустить: переменные окружения читаются при старте процесса.

docker compose up -d --force-recreate web_recipes

Вторая частая ошибка — уже после того как ALLOWED_HOSTS настроен: логин или сохранение рецепта отдаёт 403 Forbidden: CSRF verification failed. Это происходит, когда Tandoor стоит за reverse-proxy с SSL-терминацией (Traefik, nginx, Cloudflare) и не знает, что запрос на самом деле пришёл по HTTPS. Django должен явно доверять origin, с которого приходят POST-запросы:

CSRF_TRUSTED_ORIGINS=https://recipes.example.com

И убедитесь, что прокси действительно передаёт заголовок X-Forwarded-Proto: https — без него Django может считать соединение обычным HTTP, даже если снаружи всё по TLS:

location / {
    proxy_pass http://web_recipes:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Не грузятся фото рецептов и иконки

Рецепты сохраняются, но обложки не показываются или отдают 404 — это почти всегда история про статику и медиа-файлы, а не про сам Django. У Tandoor два разных типа файлов, и путают их обычно так: nginx-контейнер отдаёт /static/ (CSS, JS, иконки интерфейса), а /media/ (загруженные фото рецептов) должен быть смонтирован тем же volume-ом, что и в backend-контейнере.

Проверьте в docker-compose.yml, что оба контейнера смотрят на один и тот же именованный volume:

services:
  web_recipes:
    volumes:
      - tandoor_mediafiles:/opt/recipes/mediafiles
  nginx_recipes:
    volumes:
      - tandoor_mediafiles:/media

volumes:
  tandoor_mediafiles:

Если пути не совпадают (например, один сервис смотрит на bind-mount ./media, а другой — на именованный volume) — файлы физически лежат в разных местах, и nginx честно отдаёт 404 на то, чего у него нет.

Вторая типичная причина — права доступа. Backend-контейнер пишет файлы от своего пользователя, а если вы монтировали volume как bind-mount с host-директории с другим владельцем, запись может падать молча или с ошибкой PermissionError в логах web_recipes. Проверить и поправить:

docker compose logs web_recipes | grep -i permission
sudo chown -R 1000:1000 ./mediafiles   # UID контейнера может отличаться — смотрите в логах

Импорт рецептов по ссылке не срабатывает

Одна из главных причин ставить именно Tandoor — можно вставить ссылку на рецепт с любого кулинарного сайта, и он сам вытащит ингредиенты, шаги и фото. Работает это не всегда, и тут важно понимать, где действительно баг, а где ограничение самого механизма:

  • Сайт не в списке поддерживаемых парсеров. Tandoor использует библиотеку разбора структурированных данных (schema.org/Recipe в JSON-LD) — если сайт не публикует такую разметку, автоматический разбор просто не сработает, и это не чинится на стороне сервера.
  • Сайт отдаёт капчу или блокирует запросы без браузерных заголовков. Некоторые площадки различают запросы от бота и от браузера — в логах web_recipes при этом видна ошибка таймаута или 403 от целевого сайта, а не от Tandoor.
  • Исходящий трафик с сервера заблокирован. Проверьте банально, что контейнер вообще может выйти в интернет:
docker compose exec web_recipes curl -I https://example.com

Если curl зависает или возвращает ошибку — дело в сети/файрволе на самом сервере, а не в Tandoor. Для VPS с ограничениями исходящих соединений (актуально для некоторых бюджетных тарифов) стоит явно проверить правила iptables/ufw на исходящий трафик по 443 порту.

Когда автоимпорт не срабатывает — рецепт всегда можно добавить вручную или вставить текст, скопированный со страницы: Tandoor умеет разбирать и неструктурированный текст, просто с меньшей точностью по автоматической разбивке ингредиентов.

Резервное копирование и перенос на другой сервер

Tandoor хранит данные в двух местах, и бэкапить нужно оба, иначе восстановление будет неполным: база PostgreSQL (рецепты, пользователи, планы) и volume с медиа-файлами (фотографии).

Дамп базы:

docker compose exec db_recipes pg_dump -U tandoor -d tandoor > tandoor_db_$(date +%F).sql

Архив медиа-файлов:

docker run --rm -v tandoor_mediafiles:/data -v $(pwd):/backup alpine \
  tar czf /backup/tandoor_media_$(date +%F).tar.gz -C /data .

Оба файла стоит выгружать за пределы сервера — на объектное хранилище или хотя бы на другую машину, иначе бэкап бесполезен при полном отказе диска. Для регулярного автоматического бэкапа удобно завести отдельный cron-джоб или посмотреть в сторону BorgBackup — он умеет дедупликацию и шифрование "из коробки".

Перенос на новый сервер — это восстановление тех же двух артефактов на чистом окружении:

# на новом сервере, после docker compose up -d db_recipes
cat tandoor_db_2026-08-20.sql | docker compose exec -T db_recipes psql -U tandoor -d tandoor

docker run --rm -v tandoor_mediafiles:/data -v $(pwd):/backup alpine \
  tar xzf /backup/tandoor_media_2026-08-20.tar.gz -C /data

После восстановления обязательно проверьте .env на новом сервере — SECRET_KEY и ALLOWED_HOSTS нужно выставить заново под новый домен/IP, копировать их со старого сервера бессмысленно и небезопасно для SECRET_KEY в частности (лучше сгенерировать новый).

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Tandoor виснет сразу после docker compose up — это нормально?

Да, первые 10-20 секунд backend может перезапускаться, пока Postgres проходит инициализацию. Если контейнер не поднялся за минуту — смотрите логи web_recipes, обычно там явная причина.

Чем Tandoor принципиально отличается от Mealie?

Оба — self-hosted менеджеры рецептов, но Tandoor даёт более гибкий учёт КБЖУ по ингредиентам и группировку рецептов в книги. Если сравниваете варианты — у нас есть отдельная статья про установку Mealie на VPS.

Нужен ли Redis для Tandoor?

В части версий он используется опционально для кеширования — проверьте актуальный docker-compose.yml из официального репозитория на момент установки, состав сервисов периодически меняется между релизами.

Как сбросить пароль администратора, если забыл?

Через management-команду Django внутри контейнера: docker compose exec web_recipes python manage.py changepassword <username>.

Сколько ресурсов реально нужно под Tandoor?

Для семейного использования (2-5 пользователей) хватает 1-2 vCPU и 2 ГБ RAM с запасом — основная нагрузка на диск идёт от фотографий рецептов, а не от CPU.

Контейнер сам перезапускается в цикле (restart loop) — с чего начать диагностику?

С docker compose logs --tail=100 web_recipes и проверки, не разрослись ли логи или volume до заполнения диска — если места на диске не осталось, поведение будет похожим на сбой конфигурации. Общие причины таких падений разобраны в статье про контейнеры, которые не запускаются, а если дело именно в месте на диске — в материале о том, почему Docker занимает всё место.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →