Mise - Open-source recipe manager

Logo

Build your digital family cookbook. Store, share, and scale your favorite meals with a clean interface that stays out of your way.

View the Project on GitHub lopatnov/mise

🍽 Mise

mise en place β€” everything in its place

Open-source recipe manager you can self-host and fully own. Store and share recipes with photos, ingredients, and step-by-step instructions. Scale servings, search by text, filter by tag and category, import from any recipe URL.

CI License: GPL v3 GitHub issues GitHub stars

Screenshots

Recipe feed β€” browse, search by text, filter by tag and category, paginate

Recipe feed

Recipe detail β€” ingredients sidebar, servings scaler, step-by-step instructions with photos

Recipe detail

Tag filtering β€” active filter chip with one-click clear

Tag filter

Admin panel β€” manage users, roles, invite links, SMTP and app settings

Admin panel

Stack

Layer Technology
Backend Node.js 24, NestJS 11, TypeScript
Database MongoDB 8, Mongoose
Auth JWT Bearer, bcrypt
Frontend React 19, Vite 8, React Query 5, Zustand 5
i18n react-i18next Β· 33 languages
Reverse proxy nginx (single-port, CSP headers)
Infrastructure Docker Compose

Features


Running locally (development)

Run in this order: MongoDB first β†’ API β†’ Frontend.

Prerequisites

Tool Version Download
Git any https://git-scm.com
Node.js 24+ https://nodejs.org (LTS)
Docker Desktop any https://www.docker.com/products/docker-desktop

Step 1 β€” Clone

git clone https://github.com/lopatnov/mise.git
cd mise

Step 2 β€” Start MongoDB

docker compose up -d

MongoDB runs on localhost:27017. Data is stored in the Docker-managed volume mise_mongo_data and survives docker compose down β€” only docker compose down -v wipes it. You will never need to restart MongoDB unless you explicitly stop it.

Step 3 β€” Start the API

cd api
npm install        # first time only
npm run start:dev  # watch mode β€” restarts on file changes

Step 4 β€” Start the frontend

cd web
npm install        # first time only
npm run dev        # Vite HMR β€” updates instantly on save

Open the app, go to /setup to create the first admin account, then register users.


Configuration files

File Committed Purpose
api/.env βœ… NestJS dev config β€” MongoDB localhost, placeholder secret
web/.env βœ… Vite dev config β€” API URL for local development
.env.prod βœ… Production secrets β€” edit APP_URL and JWT_SECRET before deploying

api/.env and web/.env are used in development only. .env.prod is used only when running with docker-compose.prod.yml.


Deploying

Requirements


Pull ready-made images from GitHub Container Registry β€” no Node.js, no source code needed.

Step 1 β€” Create a working directory and configure

mkdir mise && cd mise

Create .env.prod:

APP_URL=http://YOUR_SERVER_IP   # include port if not 80, e.g. http://YOUR_SERVER_IP:8080
JWT_SECRET=<long random string> # openssl rand -hex 32
JWT_EXPIRES_IN=7d

Step 2 β€” Create docker-compose.yml

services:
  mongodb:
    image: mongo:8
    container_name: mise-mongodb
    volumes:
      - mise_mongo_data:/data/db
    environment:
      MONGO_INITDB_DATABASE: mise
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    image: ghcr.io/lopatnov/mise-api:latest
    container_name: mise-api
    depends_on:
      mongodb:
        condition: service_healthy
    env_file: .env.prod
    environment:
      MONGODB_URI: mongodb://mongodb:27017/mise
      PORT: 3000
    volumes:
      - ./data/uploads:/app/uploads
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s

  web:
    image: ghcr.io/lopatnov/mise-web:latest
    container_name: mise-web
    ports:
      - "80:80"   # change the first 80 to another port if needed
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped

volumes:
  mise_mongo_data:

Step 3 β€” Start

docker compose up -d

Go to /setup to create the admin account on first run.

SMTP β€” configure smtpHost, smtpPort, smtpUser, smtpPass, smtpFrom in the admin panel to send email verification and password-reset links. Without SMTP, links appear directly in the API response β€” useful for self-hosted installs without a mail server.

Updating to a new release

docker compose pull
docker compose up -d

Option B β€” Build from source

Step 1 β€” Clone

git clone https://github.com/lopatnov/mise.git
cd mise

Step 2 β€” Configure

Open .env.prod and set:

If port 80 is taken, change 80:80 in docker-compose.prod.yml to your port (e.g. 8080:80).

Step 3 β€” Start

docker compose -f docker-compose.prod.yml up -d --build

Go to /setup to create the admin account on first run.

Updating to a new release

git pull
docker compose -f docker-compose.prod.yml up -d --build

Stopping

# Stop containers (data is preserved)
docker compose -f docker-compose.prod.yml down

# Stop and wipe ALL data including database
docker compose -f docker-compose.prod.yml down -v

Data and backups

Data Location Notes
MongoDB Docker volume mise_mongo_data Managed by Docker, survives down
Uploaded photos ./data/uploads/ on the host Plain files, easy to copy

Back up MongoDB:

docker run --rm \
  -v mise_mongo_data:/data/db \
  -v $(pwd)/backup:/backup \
  mongo:8 mongodump --out /backup

Restore MongoDB:

docker run --rm \
  -v mise_mongo_data:/data/db \
  -v $(pwd)/backup:/backup \
  mongo:8 mongorestore /backup

Back up uploads:

cp -r ./data/uploads /your/backup/location/

Analytics & Tracking

Add any analytics script to web/index.html inside the <head> tag, before </head>.

<!-- Google Analytics example -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>

<!-- Privacy-friendly alternatives: Plausible, Umami, Matomo (self-hosted) -->

The script is embedded in the static HTML and fires on every SPA page. Rebuild the Docker image after editing (docker compose -f docker-compose.prod.yml up -d --build).


API

Swagger UI: http://localhost:3000/api/docs (dev) Β· http://YOUR_SERVER_IP/api/docs (prod)

All endpoints are prefixed with /api:

POST /api/auth/register          Register (checks allowRegistration + inviteToken)
GET  /api/auth/verify-email      Verify email address via token from registration email
POST /api/auth/login             Login β†’ JWT (requires email verification)
GET  /api/auth/me                Current user
POST /api/auth/forgot-password   Request password reset link
POST /api/auth/reset-password    Set new password via token

GET    /api/admin/setup          Check if admin exists (public)
POST   /api/admin/setup          Create first admin (public)
GET    /api/admin/settings       App settings
PATCH  /api/admin/settings       Update settings (admin only)
GET    /api/admin/users          List users (admin only)
PATCH  /api/admin/users/:id      Update role/status (admin only)
DELETE /api/admin/users/:id      Delete user (admin only)
POST   /api/admin/invites        Create invite link (admin only)
GET    /api/admin/invites        List active invites (admin only)
DELETE /api/admin/invites/:id    Revoke invite (admin only)

GET    /api/recipes              List recipes (q, tag, category, mine, page, limit)
POST   /api/recipes              Create recipe
GET    /api/recipes/:id          Get recipe (public if isPublic=true)
PATCH  /api/recipes/:id          Update recipe
DELETE /api/recipes/:id          Delete recipe
POST   /api/recipes/:id/photo    Upload main photo
POST   /api/recipes/:id/steps/:order/photo  Upload step photo
POST   /api/recipes/import-url   Import recipe data from URL (JSON-LD / Open Graph)
GET    /api/recipes/public       Public recipes (no auth)
GET    /api/recipes/tags         All distinct tags (no auth)
POST   /api/recipes/:id/saved    Save recipe to favorites
DELETE /api/recipes/:id/saved    Remove recipe from favorites

GET  /api/categories             List categories

Troubleshooting

Symptom Cause Fix
Cannot connect to Docker daemon Docker Desktop not running Open Docker Desktop and wait for the whale icon
API exits immediately Missing api/.env Create the file as shown in Step 2
MongoNetworkError MongoDB not started docker compose up -d from repo root
Frontend network errors Wrong VITE_API_URL Check web/.env
500 on login User created before isActive field existed docker exec -it mise-mongodb mongosh mise --eval 'db.users.updateMany({isActive:{$exists:false}},{$set:{isActive:true}})'
Images not loading in Docker Old named volume for uploads The volume should be a bind mount β€” see docker-compose.prod.yml
npm error ECONNRESET during --build Transient network drop from npm registry Re-run docker compose -f docker-compose.prod.yml up -d --build β€” the error is intermittent and usually clears on retry

Contributing

Contributions are welcome! Please read CONTRIBUTING.md before opening a pull request.


License

GNU General Public License v3.0 Β© 2025–2026 Oleksandr Lopatnov Β· LinkedIn