Skip to main content
  1. Services/
  2. The modern cookbook/

The modern cookbook — Phase 1: Build the cookbook

·534 words·3 mins
Table of Contents
We’re building a self‑hosted recipe library with Tandoor Recipes, running fully in Docker.

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


What we’re 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 Phase 1 of the series. Use the Series list at the end to navigate.

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: 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’re 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;  
   }  
  
}

To read more about my nginx configuration, check out this post.


Next in the series
#

Bjarne
Author
Bjarne
I’m a network engineer from Germany and like to tinker with my homelab