Build your digital family cookbook. Store, share, and scale your favorite meals with a clean interface that stays out of your way.
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.
Recipe feed β browse, search by text, filter by tag and category, paginate

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

Tag filtering β active filter chip with one-click clear

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

| 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 |
@media printRun in this order: MongoDB first β API β Frontend.
| 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 |
git clone https://github.com/lopatnov/mise.git
cd mise
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.
cd api
npm install # first time only
npm run start:dev # watch mode β restarts on file changes
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.
| 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.
docker-compose.prod.yml)Pull ready-made images from GitHub Container Registry β no Node.js, no source code needed.
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
docker-compose.ymlservices:
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:
docker compose up -d
Go to /setup to create the admin account on first run.
SMTP β configure
smtpHost,smtpPort,smtpUser,smtpPass,smtpFromin 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.
docker compose pull
docker compose up -d
git clone https://github.com/lopatnov/mise.git
cd mise
Open .env.prod and set:
APP_URL β your serverβs URL (used in email links)JWT_SECRET β long random string (openssl rand -hex 32)If port 80 is taken, change 80:80 in docker-compose.prod.yml to your port (e.g. 8080:80).
docker compose -f docker-compose.prod.yml up -d --build
Go to /setup to create the admin account on first run.
git pull
docker compose -f docker-compose.prod.yml up -d --build
# 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 | 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/
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).
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
| 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 |
Contributions are welcome! Please read CONTRIBUTING.md before opening a pull request.
GNU General Public License v3.0 Β© 2025β2026 Oleksandr Lopatnov Β· LinkedIn