Deployment
This guide covers running Momo in production. For a quick local setup, see Getting Started.
Quick Start (Docker Compose)
The recommended way to self-host Momo.
Prerequisites
- Docker Engine 24+ and Docker Compose v2
- A domain name (optional, but recommended for production)
- At least one OAuth App configured (see OAuth Setup)
Steps
- Clone the repository:
git clone https://github.com/jp1337/momo.git cd momo - Copy and configure environment variables:
cp .env.example .env.local # Edit .env.local with your credentials - Start all services:
docker compose up -d - Open
http://localhost:3000(or your domain if behind a reverse proxy)
Migrations run automatically. The container runs all pending database migrations before the Next.js server starts. Check
docker compose logs appafter deployment to confirm.
Container Images
Images are published to three registries on every release:
| Registry | Image |
|---|---|
| GitHub Container Registry | ghcr.io/jp1337/momo |
| Docker Hub | docker.io/kermit1337/momo |
| Quay.io | quay.io/jp1337/momo |
To use a pre-built image instead of building locally, replace the build section in docker-compose.yml with:
app:
image: ghcr.io/jp1337/momo:latest
Dockerfile Notes
- Multi-stage build:
deps→builder→runner - Base image:
node:24-alpine(Node.js 24 LTS, supported until April 2028) - Runs as non-root user (
nextjs:1001) - Uses
output: standalonefor a minimal production bundle - No dev dependencies in the final image
docker-entrypoint.shrunsscripts/migrate.mjson every container start — applies any pending Drizzle migrations before the server starts- HEALTHCHECK hits
/api/healthevery 30 seconds (start-period: 30sto allow migrations to finish)
Health Endpoint
GET /api/health requires no authentication and returns:
| Feld | Bedeutung |
|---|---|
status |
ok, oder error bei nicht erreichbarer Datenbank (HTTP 503) |
version |
Version des laufenden Images (aus package.json) |
commit |
Git-Commit-SHA, aus dem das laufende Image gebaut wurde (Build-Arg MOMO_COMMIT, siehe Dockerfile); null außerhalb eines gebauten Images (lokale Entwicklung) |
timestamp |
Zeitpunkt der Antwort |
cron |
Letzter Cron-Lauf; rein informativ, nie Grund für einen Fehlerstatus |
version und commit stehen auch in der 503-Fehlerantwort — beide brauchen keine
Datenbank.
Die Publish-Pipeline (.github/workflows/build-and-publish.yml, Job deploy) fragt
das Feld commit nach jedem Release-Tag v* ab, um zu bestätigen, dass Watchtower
den Container tatsächlich getauscht hat — Watchtowers HTTP 200 sagt nur, dass die
Anfrage angenommen wurde, nicht dass der neue Container läuft.
Auf Release-Tags und nicht auf jedem Push nach main, weil die Instanz dem
Minor-Serien-Tag folgt (ghcr.io/jp1337/momo:0.7) und nicht :latest: ein Patch-Release
rollt damit von selbst, ein Minor-Sprung bleibt eine bewusste Handlung. Ein gewöhnlicher
Push nach main bewegt nur :latest und :sha-…, also nichts, dem die Instanz folgt.
Verglichen wird commit gegen github.sha, nicht version: commit erkennt auch ein
neu gebautes Image unter derselben Version, wo ein Versionsvergleich sofort grün wäre.
Der Schritt liest die zu prüfende URL aus der Repository-Variable ROLLOUT_HEALTH_URL
(Fallback: NEXT_PUBLIC_APP_URL, dann https://momotask.app). Läuft der Runner in einem
Intranet ohne Zugriff auf die öffentliche URL, setze ROLLOUT_HEALTH_URL auf eine intern
erreichbare Adresse derselben Instanz — immer mit Schema (https://…, http://…).
Ohne Schema landet curl auf einer 301-HTML-Seite des Reverse Proxy, findet kein commit
und läuft in seine vollen zehn Minuten, egal wie sauber der Rollout war.
Production Checklist
Before going live, complete all items below:
- Generate AUTH_SECRET — must be at least 32 random bytes:
openssl rand -base64 32 - Set
AUTH_TRUST_HOST=true— required when the app runs behind any reverse proxy (nginx, Caddy, Traefik) or in Kubernetes. Auth.js v5 rejects requests from unrecognised hosts unless this is set. - Set all required environment variables — see Environment Variables
- Generate VAPID keys for push notifications:
npx web-push generate-vapid-keys - Set
NEXT_PUBLIC_APP_URLandNEXTAUTH_URLto your real public HTTPS origin — both feed OAuth callbacks, notification links, and SEO output (metadataBase,robots.txt,sitemap.xml, Open Graph tags, JSON-LD). Leaving these athttp://localhost:3000in production means search engines and link previews will indexlocalhost.- Important for
NEXT_PUBLIC_APP_URL: this variable is a Next.jsNEXT_PUBLIC_*— its value is inlined into the client bundle and the pre-rendered HTML at build time. Setting it only at runtime (viadocker run -e ...) affects the dynamic surfaces (sitemap, robots.txt, iCal feed, WebAuthn) but not the baked-in Open Graph / JSON-LD tags. You need to bake it into the image:# Plain Docker docker build --build-arg NEXT_PUBLIC_APP_URL=https://your-domain.com -t momo . # Docker Compose — set NEXT_PUBLIC_APP_URL in your .env file and then: docker compose build docker compose up -d - The published
ghcr.io/jp1337/momoimage bakes inhttps://momotask.app. Self-hosters using a different public URL must build their own image. See the SEO guide for the full rationale.
- Important for
- Register OAuth apps for your production domain (callback URLs must match):
- GitHub:
https://your-domain.com/api/auth/callback/github - Discord:
https://your-domain.com/api/auth/callback/discord - Google:
https://your-domain.com/api/auth/callback/google - Microsoft (private accounts):
https://your-domain.com/api/auth/callback/microsoft-entra-id - Generic OIDC (Authentik, Keycloak, …):
https://your-domain.com/api/auth/callback/keycloak
- GitHub:
- Set CRON_SECRET to protect cron endpoints from unauthorized access:
openssl rand -hex 32 - (Optional) Configure SMTP — if you want the Email notification channel, set
SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASS,SMTP_FROM, andSMTP_SECURE. Without these the channel is hidden in the UI. See Environment Variables. - (Optional) Set
TOTP_ENCRYPTION_KEY— required as soon as any user enables 2FA. AES-256-GCM key for encrypting TOTP secrets at rest. Generate withopenssl rand -hex 32. Treat as critical secret material — rotating it forces every user to re-enroll their authenticator app. See Two-Factor Authentication. - (Optional) Set
REQUIRE_2FA=true— forces every user (including existing ones) to register a second factor (TOTP or Passkey) before they can access any protected route. Existing users are hard-locked to a forced setup page on next login. See Self-Hosting → Enforcing two-factor authentication. - (Optional) Set
WEBAUTHN_RP_ID+WEBAUTHN_RP_NAME— for the Passkey feature.WEBAUTHN_RP_IDdefaults to the hostname ofNEXT_PUBLIC_APP_URL; only set it explicitly if your site lives on a subdomain that needs a different eTLD+1.WEBAUTHN_RP_NAMEis the cosmetic name shown in the OS / browser passkey prompt (defaultMomo). See Passkeys. - (Optional) Set ADMIN_USER_IDS — comma-separated user UUIDs that can access the
/adminaggregate-statistics page. Leave empty to disable the admin page entirely. - Configure TLS — use a reverse proxy (nginx, Caddy) or cert-manager in Kubernetes
- Configure HSTS — the app sets
Strict-Transport-Securityheaders automatically - Never commit real secrets to git — use
.env.local(gitignored) or a secrets manager - Migrations run automatically — the container applies all pending migrations on startup. No manual step needed; check
docker compose logs appto verify. - Schedule the cron dispatcher — production deployments need a periodic call to
POST /api/cronfor daily-quest selection, streak reminders, and the weekly review push. Docker Compose users can add a tinycurlcron container; Kubernetes users get a ready-madecronjob.yaml(see the Kubernetes guide).
Reverse Proxy with Caddy
Caddy is the simplest way to add HTTPS in front of Momo. Create a Caddyfile:
momo.example.com {
reverse_proxy localhost:3000
}
Caddy automatically provisions a Let’s Encrypt certificate. Run:
caddy run --config Caddyfile
Important: When running behind Caddy (or any reverse proxy), set
AUTH_TRUST_HOST=truein.env.local. Auth.js v5 requires this to accept requests forwarded by the proxy.
Reverse Proxy with nginx
server {
listen 443 ssl;
server_name momo.example.com;
ssl_certificate /etc/letsencrypt/live/momo.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/momo.example.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
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;
}
}
Important: When running behind nginx (or any reverse proxy), set
AUTH_TRUST_HOST=truein.env.local. Auth.js v5 requires this to accept requests forwarded by the proxy.
AUTH_SECRET Rotation
To rotate the AUTH_SECRET (e.g. after a suspected compromise):
- Generate a new secret:
openssl rand -base64 32 - Update the secret in your environment:
- Docker Compose: update
AUTH_SECRETin.env.local, then restart the app:docker compose up -d app - Kubernetes: update the Secret and trigger a rollout:
kubectl patch secret momo-secrets -n momo \ --type=merge \ -p '{"stringData":{"AUTH_SECRET":"<new-secret>"}}' kubectl rollout restart deployment/momo-app -n momo
- Docker Compose: update
-
Effect: All existing sessions are immediately invalidated. All users will be signed out and must log in again. This is expected and safe.
- Frequency: Rotate at minimum once per year. Rotate immediately if the secret is exposed.
Kubernetes
For full Kubernetes deployment instructions, see the Kubernetes guide.
The example manifests are in deploy/examples/ in the repository:
| File | Purpose |
|---|---|
namespace.yaml |
Creates the momo namespace |
deployment.yaml |
App deployment with 2 replicas, liveness/readiness probes |
service.yaml |
ClusterIP service for the app |
ingress.yaml |
Ingress with TLS (cert-manager + ingress-nginx) |
secret.example.yaml |
Template for required Kubernetes secrets |
postgres-statefulset.yaml |
PostgreSQL 18 StatefulSet with persistent volume |
cronjob.yaml |
Kubernetes CronJob calling POST /api/cron for daily quest, streak reminders, and the weekly review |