Skip to content

Deploying

This guide covers three production deployment paths. For local development setup, see Installation.


Every deployment needs the same core environment variables regardless of path:

VariableDescription
DATABASE_URLPostgreSQL connection string
NEXTAUTH_URLFull public URL of your deployment
NEXTAUTH_SECRETRandom secret — generate with openssl rand -base64 32
GOOGLE_CLIENT_IDGoogle OAuth client ID
GOOGLE_CLIENT_SECRETGoogle OAuth client secret

See Configuration for the full variable reference, including AI provider keys and storage options.

Update your Google OAuth credentials to include your production domain:

  1. Go to console.cloud.google.com/apis/credentials and open your OAuth client.
  2. Add your production URL to Authorized JavaScript origins (e.g. https://your-domain.com).
  3. Add the production callback URL to Authorized redirect URIs: https://your-domain.com/api/auth/callback/google.

Every route in OpenCauldron requires authentication except two:

  • /api/auth/* — NextAuth sign-in and callback endpoints
  • /api/uploads/* — Local file serving for the local storage backend

Unauthenticated requests to any other route are redirected to /login. This applies to the API and to all page routes.

To restrict sign-in to a single email domain, set:

Terminal window
ALLOWED_EMAIL_DOMAIN="yourcompany.com"

Leave this unset to allow any Google account to sign in.


Docker Compose is the simplest self-hosted path. The included docker-compose.yml starts the app and a Postgres 16 database together, with a health check so the app waits for the database to be ready.

Docker Compose reads from .env.local — not .env. Copy the example and fill in your values:

Terminal window
cp .env.example .env.local

Set at minimum:

Terminal window
DATABASE_URL="postgresql://cauldron:cauldron@db:5432/cauldron"
NEXTAUTH_URL="https://your-domain.com"
NEXTAUTH_SECRET="" # openssl rand -base64 32
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
STORAGE_PROVIDER="local"

The DATABASE_URL host must be db (the Compose service name), not localhost.

Terminal window
docker compose up -d

This builds the app image, starts Postgres, waits for it to be healthy, then starts the app on port 3000.

On first deploy, apply the database schema:

Terminal window
docker compose exec app bun run db:migrate

Badge definitions (feats) must be inserted into the database before they appear in the UI. The seed script reads from .env.local and requires a Neon connection string — if you are running a plain Postgres database, run the seed command from a separate environment that has a Neon DATABASE_URL, or skip this step and the feats system will simply be empty until you can run it.

To seed from a machine with a Neon DATABASE_URL in .env.local:

Terminal window
bun src/lib/db/seed-badges.ts

Re-run this after upgrades to pick up any new or renamed badges.

The docker-compose.yml defines two named volumes:

  • pgdata — Postgres data directory, persists database across container restarts
  • uploads — mounted at /app/uploads in the app container, persists locally stored files

As long as you do not remove these volumes, your data survives container rebuilds and restarts.

To deploy a new version:

Terminal window
docker compose build
docker compose up -d
docker compose exec app bun run db:migrate

Use this path when you want to manage the database externally (Neon, Supabase, RDS, or any hosted Postgres) and run only the app container.

The Dockerfile uses a three-stage build:

  1. deps — installs dependencies with bun install --frozen-lockfile
  2. builder — runs bun run build, which produces a Next.js standalone output at .next/standalone
  3. runner — copies only the standalone bundle, static files, and public/ into a slim image

The production container starts with bun server.js, not next start. The standalone output bundles the Node server directly, so next does not need to be installed in the final image.

Terminal window
docker run -d \
-p 3000:3000 \
--env-file .env \
ghcr.io/opencauldron/opencauldron:latest

Or pass variables individually:

Terminal window
docker run -d \
-p 3000:3000 \
-e DATABASE_URL="postgresql://user:pass@host/db?sslmode=require" \
-e NEXTAUTH_URL="https://your-domain.com" \
-e NEXTAUTH_SECRET="your-secret" \
-e GOOGLE_CLIENT_ID="your-client-id" \
-e GOOGLE_CLIENT_SECRET="your-client-secret" \
-e STORAGE_PROVIDER="r2" \
-e R2_ACCOUNT_ID="your-account-id" \
-e R2_ACCESS_KEY_ID="your-access-key" \
-e R2_SECRET_ACCESS_KEY="your-secret-key" \
-e R2_BUCKET_NAME="cauldron" \
-e R2_PUBLIC_URL="https://your-bucket.your-domain.com" \
ghcr.io/opencauldron/opencauldron:latest

The app listens on port 3000. Map it to whichever port your reverse proxy expects.

Provide DATABASE_URL pointing to your external Postgres instance. Run migrations before starting the container (or as a separate init step):

Terminal window
DATABASE_URL="postgresql://..." bun run db:migrate

The local filesystem backend does not work well with standalone containers — the /app/uploads directory is ephemeral unless you mount a volume. For containerized deployments without Compose, use R2:

Terminal window
STORAGE_PROVIDER="r2"

See the Storage guide for full R2 configuration.


Vercel is the easiest path if you do not need self-hosting. The app deploys as a serverless Next.js application.

Before deploying to Vercel you need:

  • A Neon database (or another serverless-compatible Postgres)
  • A Cloudflare R2 bucket with public access enabled

Both are required. Read on for why.

OpenCauldron auto-detects the database driver at startup. If DATABASE_URL contains neon.tech or neon.db, it uses the @neondatabase/serverless HTTP driver, which is compatible with Vercel’s edge and serverless runtime. Standard pg connections use long-lived TCP connections that do not work in serverless environments. Use a Neon connection string for Vercel deployments.

Vercel’s serverless functions have an ephemeral filesystem — any files written to disk disappear when the function exits. The local storage backend writes to disk and will not work on Vercel. Set STORAGE_PROVIDER="r2" and configure your R2 credentials.

Additionally, image-to-video generation requires a publicly accessible URL for the reference image. R2 with a public bucket satisfies this requirement; local storage does not.

  1. Push your fork to GitHub.
  2. Import the repository in the Vercel dashboard.
  3. Add environment variables in Settings > Environment Variables.

Set all required variables plus:

Terminal window
NEXTAUTH_URL="https://your-project.vercel.app" # or your custom domain
STORAGE_PROVIDER="r2"

Vercel automatically runs bun run build (or next build) during deployment. No extra build command is needed.

Add every variable from .env.example that applies to your deployment. Key ones to double-check:

VariableValue for Vercel
DATABASE_URLNeon connection string (must contain neon.tech)
NEXTAUTH_URLYour production URL — must match the deployed domain exactly
STORAGE_PROVIDERr2
R2_PUBLIC_URLPublic base URL for your R2 bucket

Vercel does not run migrations automatically. Run them from your local machine against your Neon database before or after the first deploy:

Terminal window
DATABASE_URL="postgresql://..." bun run db:migrate

Then seed badge definitions:

Terminal window
# .env.local must contain your Neon DATABASE_URL
bun src/lib/db/seed-badges.ts

After deploying, update NEXTAUTH_URL in Vercel’s environment variables to match your custom domain, and update the Authorized JavaScript origins and Authorized redirect URIs in your Google OAuth credentials to include the new domain.


  • Configuration — Full environment variable reference
  • Storage — R2 setup and storage backend details
  • API Keys — Configure AI provider keys