Skip to main content
  1. Projects/
  2. Self-hosted recipe planner: recipes, AI agents, and grocery automation/

Self-hosted recipe planner - Part 1: Set up Tandoor Recipes

·659 words·4 mins
Self-hosted Recipe Planner - This article is part of a series.
Part 1: This Article
We are building a self‑hosted recipe library with Tandoor Recipes, running fully in Docker. This is Part 1 of the series.

Tandoor Recipes is open source, mobile‑friendly, supports imports and tagging, and exposes a solid API for later automation.


What we are building
#

  • PostgreSQL for data
  • Tandoor web app (UI + API)
  • Nginx for static and request handling

Everything runs in Docker for simple deploy and maintenance.

This is Part 1 of the Self-hosted Recipe Planner series. Use the series links at the bottom to navigate between parts.

Setup with Docker Compose
#

Prerequisites
#

  • Docker and Docker Compose installed on your server (any modern Linux; this example uses a Ubuntu VM on Proxmox)
  • A domain name pointing to your server
If docker-compose errors with “ModuleNotFoundError: No module named ‘distutils’”, install python3-setuptools.
bjarne@recipe:/opt/recipe$ sudo docker-compose up -d  
Traceback (most recent call last):  
 File "/usr/bin/docker-compose", line 33, in <module>  
   sys.exit(load_entry_point('docker-compose==1.29.2', 'console_scripts', 'docker-compose')())  
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  
 File "/usr/bin/docker-compose", line 25, in importlib_load_entry_point  
   return next(matches).load()  
          ^^^^^^^^^^^^^^^^^^^  
 File "/usr/lib/python3.12/importlib/metadata/__init__.py", line 205, in load  
   module = import_module(match.group('module'))  
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  
 File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module  
   return _bootstrap._gcd_import(name[level:], package, level)  
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  
 File "<frozen:importlib._bootstrap>", line 1387, in _gcd_import  
 File "<frozen:importlib._bootstrap>", line 1360, in _find_and_load  
 File "<frozen:importlib._bootstrap>", line 1331, in _find_and_load_unlocked  
 File "<frozen:importlib._bootstrap>", line 935, in _load_unlocked  
 File "<frozen:importlib._bootstrap_external>", line 995, in exec_module  
 File "<frozen:importlib._bootstrap>", line 488, in _call_with_frames_removed  
 File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 9, in <module>  
   from distutils.spawn import find_executable  
ModuleNotFoundError: No module named 'distutils'

Install the python3-setuptools package:

sudo apt install python3-setuptools

Compose file
#

Create a directory (e.g., /opt/recipes/) and add docker-compose.yml.

services:
  db_recipes:
    restart: always
    image: postgres:16-alpine
    volumes:
      - ./postgresql:/var/lib/postgresql/data
    env_file:
      - ./.env

  web_recipes:
    restart: always
    image: vabene1111/recipes
    env_file:
      - ./.env
    volumes:
      - staticfiles:/opt/recipes/staticfiles
      # Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
      - nginx_config:/opt/recipes/nginx/conf.d
      - ./mediafiles:/opt/recipes/mediafiles
    depends_on:
      - db_recipes

  nginx_recipes:
    image: nginx:mainline-alpine
    restart: always
    ports:
      - 8000:80
    env_file:
      - ./.env
    depends_on:
      - web_recipes
    volumes:
      # Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
      - nginx_config:/etc/nginx/conf.d:ro
      - staticfiles:/static:ro
      - ./mediafiles:/media:ro

volumes:
  nginx_config:
  staticfiles:

Running
#

Now we can run the Docker Compose file:

[!NOTE] First start pulls images and creates volumes; subsequent starts are fast.

bjarne@recipe:/opt/recipe$ sudo docker-compose up -d  
Creating network "recipe_default" with the default driver  
Creating volume "recipe_nginx_config" with default driver  
Creating volume "recipe_staticfiles" with default driver  
Pulling db_recipes (postgres:16-alpine)...  
16-alpine: Pulling from library/postgres  
f18232174bc9: Pull complete  
10e9080a2e24: Pull complete  
8adafa230647: Pull complete  
6e354c51bc01: Pull complete  
f71a8df96436: Pull complete  
162e443ce030: Pull complete  
4c3435ecd36c: Pull complete  
9842d12f7042: Pull complete  
f0c6e23b5b51: Pull complete  
6dc008d32b79: Pull complete  
8814fd72acf5: Pull complete  
Digest: sha256:7c8c4bf319769e3daf2545ba435248edc650d3621de678db6d9846581036e3da  
Status: Downloaded newer image for postgres:16-alpine  
Pulling nginx_recipes (nginx:mainline-alpine)...  
mainline-alpine: Pulling from library/nginx  
f18232174bc9: Already exists  
61ca4f733c80: Pull complete  
b464cfdf2a63: Pull complete  
d7e507024086: Pull complete  
81bd8ed7ec67: Pull complete  
197eb75867ef: Pull complete  
34a64644b756: Pull complete  
39c2ddfd6010: Pull complete  
Digest: sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10  
Status: Downloaded newer image for nginx:mainline-alpine  
Creating recipe_db_recipes_1 ... done  
Creating recipe_web_recipes_1 ... done  
Creating recipe_nginx_recipes_1 ... done

Nginx reverse proxy
#

If you are fronting with an external Nginx, proxy to the Compose Nginx at port 8000.

server {  
   listen 443 ssl;  
   server_name rezepte.kohnkenet.de;  
  
   error_log  /var/log/nginx/rezepte.error.log  warn;  
   access_log  /var/log/nginx/rezepte.access.log;  
  
   location / {  
       proxy_http_version 1.1;  
       proxy_set_header Upgrade $http_upgrade;  
       proxy_set_header Connection "Upgrade";  
  
       proxy_set_header X-Forwarded-Host $http_host;  
       proxy_set_header Host $http_host;  
       proxy_set_header X-Forwarded-Proto $scheme;  
  
       proxy_pass       http://recipe.kohnkenet.de:8000;  
   }  
  
}

What we have now
#

At this point you should have a working Tandoor instance at your domain. You can:

  • Create an admin account and log in
  • Import recipes from URLs (Tandoor has a built-in scraper)
  • Add recipes manually with ingredients and instructions
  • Organize everything with tags, categories, and keywords
  • Use the meal plan feature to schedule recipes for specific days

This is the foundation. The recipe library, the API, and the meal planner - all running on your hardware.


Next up
#

In Part 2 we connect Remy - an AI agent - to your new Tandoor instance and Bring!, so you can plan meals and build shopping lists from a chat window.

Self-hosted recipe planner - Part 2: Connect Remy, your AI recipe agent
·1051 words·5 mins
Bjarne
Author
Bjarne
Network engineer from Germany. I write about networking, Linux, security, self-hosting, and homelab projects.
Self-hosted Recipe Planner - This article is part of a series.
Part 1: This Article