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:22-alpine(Node.js 22 LTS, supported until April 2027) - 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)
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 |