Skip to content

MartinSantosT/myzest

Repository files navigation

Zest Logo

Zest

The lightest self-hosted recipe manager that actually cares about your food memories.

Docker License Python SQLite Stars Issues Ask DeepWiki

Other recipe apps store recipes. Zest stores the moments around the table.

Quickstart →  ·  User Manual  ·  Issues

Built for homelab users, privacy-focused cooks, and anyone who believes their recipes — and the stories behind them — should live on their own hardware.


Zest Demo — Recipes, Memories, Moment Cards

Recipe Collection — Dark Mode


Why Zest?

Every recipe manager out there treats food like data: title, ingredients, steps, done. But cooking is more than that. It's the Sunday your daughter helped you make pasta for the first time. The improvised dinner that became a family legend. The photo your partner took while you weren't looking.

Zest is a self-hosted culinary diary — recipes + memories + beautiful shareable cards, all running on your own hardware in a single container. Deploy on a Raspberry Pi, Proxmox LXC, NAS, or any Linux VPS with one docker compose up. No cloud, no subscriptions, no telemetry. Just your food story, owned by you.

What makes it different

  • Memories — Attach photos, dates, locations, and stories to any recipe. Build a timeline of your cooking life.
  • Moment Cards — Generate gorgeous shareable images (Instagram Stories, square, landscape) from your memories with one click.
  • Cookbooks — Curate collections and share them via public links or export as beautiful PDFs.
  • "What to Cook?" — Tell Zest what ingredients you have, it tells you what you can make.
  • 4-Tier Recipe Scraper — Paste any URL. Zest tries recipe-scrapers, JSON-LD, Microdata, then HTML heuristics. It will find the recipe.
  • Dead simple to run — One docker compose up. SQLite. No Postgres, no Redis, no external services.

Features at a glance

Feature Description
🍳 Full Recipe CRUD Create, edit, search, filter, rate, favorite. Categories and color-coded tags.
🌐 URL Import Paste a link from any cooking site. 4-tier scraping handles even the worst recipe blogs.
📸 Memories Photo diary entries linked to recipes. EXIF support, GPS metadata, multi-photo upload.
🎴 Moment Cards Auto-generated social cards (1080×1080, 1080×1920, 1200×630) from your memories.
📚 Cookbooks Curated collections with custom covers. Share via link or export as PDF.
🔗 Sharing Public links for cookbooks. Recipients don't need an account.
🛒 Shopping List Add ingredients from any recipe, scaled to your portions. Combine multiple recipes.
⚖️ Portion Calculator Scale any recipe from 1 to 100 servings. Fractions displayed beautifully (½, ¼, ⅓).
👨‍🍳 Cooking Mode Full-screen step-by-step with auto-detected timers, Wake Lock, and ingredient panel.
🔍 "What to Cook?" Enter available ingredients → get ranked recipe suggestions with missing item counts.
📄 PDF Export Professional cookbook PDFs with cover page, recipe cards, and proper typography.
💾 Automatic Backups Scheduled database + image backups. Manual export/import. Full disaster recovery.
🌙 Dark Mode Full dark theme. Respects your preference.
📱 Mobile Friendly Responsive design for desktop and mobile.
🔒 Auth + Rate Limiting JWT auth, bcrypt passwords, rate-limited endpoints.
🍎 iPhone Photos HEIC/HEIF support out of the box.

Quickstart

git clone https://github.com/MartinSantosT/myzest.git
cd myzest
cp .env.example .env
docker compose up -d

Open http://localhost:8000 and that's it. First-time setup takes about 30 seconds.

First launch: when you visit a fresh Zest install, the server detects there are no users yet and redirects you to /welcome — a one-time setup screen. The first user that registers becomes admin and gets 12 example recipes with photos to explore.

Generate a secure secret key

python -c "import secrets; print(secrets.token_urlsafe(32))"

Paste the result in your .env file:

ZEST_SECRET_KEY=your-generated-secret-here

Architecture

myzest/
├── app/
│   ├── main.py              # FastAPI application (all endpoints)
│   ├── models.py             # SQLAlchemy models
│   ├── schemas.py            # Pydantic schemas
│   ├── database.py           # Database connection
│   ├── scrapers.py           # 4-tier recipe scraping engine
│   ├── card_generator.py     # Pillow-based moment card generator
│   └── static/
│       ├── index.html        # SPA frontend
│       ├── shared.html       # Public cookbook viewer
│       ├── uploads/          # User photos (volume-mounted)
│       └── js/               # 26 ES modules
│           ├── app.js        # Module orchestrator
│           ├── api.js         # API client
│           ├── recipes.js    # Recipe management
│           ├── memories.js   # Culinary diary
│           ├── cookbooks.js  # Collections
│           ├── shoppingList.js
│           ├── calculator.js
│           ├── whatToCook.js
│           └── ...           # 18 more modules
├── tests/                    # pytest test suite
├── data/                     # SQLite database + backups (volume-mounted)
├── screenshots/              # Repo screenshots (excluded from Docker image)
├── Dockerfile
├── docker-compose.yml
├── requirements.txt          # Runtime deps (pinned)
├── requirements-dev.txt      # Test deps (pytest, httpx)
├── .dockerignore
├── .env.example
├── MANUAL.md                 # User manual
└── README.md

Stack: FastAPI · SQLAlchemy · SQLite · Pillow · ReportLab · Vanilla JS (ES Modules) · Tailwind CSS

No frontend build step. No webpack. No node_modules. The frontend is a single HTML file with 26 ES modules loaded natively by the browser.


Configuration

All configuration is done through environment variables in .env:

Variable Default Description
ZEST_SECRET_KEY (empty) JWT signing key. Generate a unique value with python3 -c "import secrets; print(secrets.token_urlsafe(32))" and put it in your .env. If left empty, Zest auto-generates one in data/.secret_key and logs a warning until you set it explicitly. See MANUAL.md → "Secure your instance" for details.
ALLOW_PUBLIC_REGISTRATION true Whether new users can register after the first admin is created. Default true is suitable for a family or a homelab on a closed network. Set false if you expose Zest to the public internet — only the admin can register, additional users must be invited (feature coming soon). The very first user (initial setup) is always allowed regardless of this setting. See MANUAL.md → "Public registration".
ALLOW_PRIVATE_NETWORKS false Allow the recipe scraper to fetch URLs that resolve to private IP ranges (10.x, 172.16.x, 192.168.x). Disabled by default to prevent SSRF attacks. Enable only if you intentionally want to scrape recipes from another service in your own LAN. Loopback (127.x) and cloud metadata endpoints (169.254.x) remain blocked even when this is enabled. See MANUAL.md for details.

Volumes

Path Purpose
./data:/app/data SQLite database, automatic backups, the .secret_key fallback file
./app/static/uploads:/app/app/static/uploads User-uploaded photos (recipes, memories, cookbook covers, avatars)

For a complete file-system backup, snapshot both paths. The in-app backup feature (Settings → Backups) packages them into a single ZIP automatically.

Ports

Default port is 8000. Change it in docker-compose.yml:

ports:
  - "3000:8000"  # Access on port 3000

Updating

cd myzest
git pull
docker compose down && docker compose up -d --build

Your data is preserved. Zest stores everything in volume-mounted directories on your host:

  • ./data/ — SQLite database, automatic backups, the persisted secret key
  • ./app/static/uploads/ — your uploaded photos

docker compose down only stops the container; it does not touch these directories. The new container starts back up against the same files. Database schema migrations run automatically on startup (PRAGMA journal_mode=WAL is also enabled for better multi-user concurrency).

Belt and suspenders: Zest also creates an automatic backup before any in-app restore (Settings → Backups). For peace of mind on a major upgrade, you can manually export a full backup ZIP first via Settings → Backups → "Export full backup".

Pinning to a specific version (optional): instead of git pull, run git fetch --tags && git checkout v2.1.3 to lock to a known good release. Use git checkout main && git pull to come back to the latest.

How do I know there is a new version? When the running app version differs from the version stored in your browser, Zest shows an orange banner in the bottom-right of /app saying "New version available (vX.Y.Z) — Reload". Clicking Reload refreshes the page so the new client assets are loaded.


Backups

Zest includes a built-in backup system accessible from Settings → Backups:

  • Automatic backups — Configure frequency (12h / 24h / 7 days) and retention count
  • Manual backups — One-click database + images export as .zip
  • Import/Export — Move your data between instances or restore from backup
  • JSON export — Export recipes as portable JSON for interoperability

Backups are stored in ./data/backups/ on the host.


Running tests

Tests are not shipped in the production image. Run them locally with the dev requirements:

pip install -r requirements-dev.txt
pytest tests/ -v

Or, against the running container (the test suite is mounted from the repo, not from the image):

docker run --rm -v "$PWD":/app -w /app python:3.11-slim sh -c "pip install -r requirements-dev.txt && pytest tests/ -v"

Reverse proxy (optional)

Example Nginx config for exposing Zest with HTTPS:

server {
    listen 443 ssl http2;
    server_name zest.example.com;  # e.g. myzest.app

    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    client_max_body_size 20M;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Works great behind Caddy, Traefik, or Nginx Proxy Manager too.


Health check

curl http://localhost:8000/api/health
# {"status": "healthy", "recipes": 42, "users": 1}

Perfect for Uptime Kuma, Healthchecks.io, or any monitoring tool.


Tech details for the curious

  • Recipe scraping uses a 4-tier fallback: recipe-scrapers library (400+ sites) → JSON-LD structured data → Microdata attributes → CSS heuristic selectors. If a recipe exists on the page, Zest will find it.
  • Moment Cards are generated server-side with Pillow. No external APIs, no Canvas tricks. Pure Python image generation with gradient overlays, typography, and branding.
  • PDF export uses ReportLab with DejaVu fonts (bundled in the Docker image) for proper Unicode support.
  • Auth uses bcrypt with automatic legacy hash migration. Rate limiting via slowapi on sensitive endpoints.
  • Frontend is a true SPA built with vanilla JavaScript ES modules. Zero dependencies. Zero build tools. Fast.
  • Image processing automatically resizes uploads to max 1920px, converts HEIC to JPEG, and preserves EXIF metadata.

Contributing

Found a bug or have a feature idea? Open an issue or submit a PR — contributions are welcome.


License

MIT — do whatever you want with it.


🍊 Zest — Because recipes deserve memories.