GitHub ↗

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

  1. Clone the repository:
    git clone https://github.com/jp1337/momo.git
    cd momo
    
  2. Copy and configure environment variables:
    cp .env.example .env.local
    # Edit .env.local with your credentials
    
  3. Start all services:
    docker compose up -d
    
  4. 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 app after 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: depsbuilderrunner
  • Base image: node:22-alpine (Node.js 22 LTS, supported until April 2027)
  • Runs as non-root user (nextjs:1001)
  • Uses output: standalone for a minimal production bundle
  • No dev dependencies in the final image
  • docker-entrypoint.sh runs scripts/migrate.mjs on every container start — applies any pending Drizzle migrations before the server starts
  • HEALTHCHECK hits /api/health every 30 seconds (start-period: 30s to 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_URL and NEXTAUTH_URL to 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 at http://localhost:3000 in production means search engines and link previews will index localhost.
    • Important for NEXT_PUBLIC_APP_URL: this variable is a Next.js NEXT_PUBLIC_* — its value is inlined into the client bundle and the pre-rendered HTML at build time. Setting it only at runtime (via docker 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/momo image bakes in https://momotask.app. Self-hosters using a different public URL must build their own image. See the SEO guide for the full rationale.
  • 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
  • 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, and SMTP_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 with openssl 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_ID defaults to the hostname of NEXT_PUBLIC_APP_URL; only set it explicitly if your site lives on a subdomain that needs a different eTLD+1. WEBAUTHN_RP_NAME is the cosmetic name shown in the OS / browser passkey prompt (default Momo). See Passkeys.
  • (Optional) Set ADMIN_USER_IDS — comma-separated user UUIDs that can access the /admin aggregate-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-Security headers 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 app to verify.
  • Schedule the cron dispatcher — production deployments need a periodic call to POST /api/cron for daily-quest selection, streak reminders, and the weekly review push. Docker Compose users can add a tiny curl cron container; Kubernetes users get a ready-made cronjob.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=true in .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=true in .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):

  1. Generate a new secret:
    openssl rand -base64 32
    
  2. Update the secret in your environment:
    • Docker Compose: update AUTH_SECRET in .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
      
  3. Effect: All existing sessions are immediately invalidated. All users will be signed out and must log in again. This is expected and safe.

  4. 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