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.
