Deployment

Ship the Next.js template to production with Vercel or Docker.

The template builds in Next.js standalone output mode and ships with a multi-stage Dockerfile. This page covers the quality gate, both deployment targets, and a pre-launch checklist.

Quality gate

Run before every deploy:

pnpm lint
pnpm build

pnpm build produces the standalone server used by Docker.

Build-time vs. runtime configuration

NEXT_PUBLIC_* variables are inlined at build time and cannot be changed by the running container — they must be present when pnpm build runs, and changing them requires a rebuild. Server-only secrets (DATABASE_URL, BETTER_AUTH_SECRET, CREEM_API_KEY, RESEND_API_KEY, etc.) are read at runtime. Plan your pipeline so public IDs (e.g. NEXT_PUBLIC_CREEM_PRODUCT_*) are set at build, not just at runtime.

Vercel

  1. Import the repository in Vercel.
  2. Add the environment variables — build-time NEXT_PUBLIC_* and runtime secrets alike.
  3. Provision a hosted PostgreSQL with pooling and set DATABASE_URL.
  4. Set BETTER_AUTH_URL to your production domain and register OAuth callbacks.

Vercel's filesystem is ephemeral. The local image upload writes to public/uploads, which does not persist on Vercel. Move uploads to object storage before deploying upload-dependent features — see Storage.

Docker / self-hosting

next.config.mjs sets output: "standalone", and the Dockerfile is a multi-stage build:

  • Base image node:22-alpine, with pnpm via Corepack.
  • deps installs with --frozen-lockfile; builder runs pnpm build.
  • runner runs as a non-root user (nextjs, uid 1001) and copies public, .next/standalone, and .next/static.
  • Exposes port 3000, sets HOSTNAME=0.0.0.0, and starts node server.js.
docker build -t soar-next .
docker run -p 3000:3000 --env-file .env soar-next

Provide runtime secrets via --env-file/orchestrator secrets, and make sure build-time NEXT_PUBLIC_* values are available during docker build.

Pre-launch checklist

  • Database — hosted PostgreSQL with pooling; a real migration workflow (not just drizzle-kit push — see Database).
  • Better Auth — production BETTER_AUTH_URL and a fresh, secret BETTER_AUTH_SECRET.
  • OAuth — production callback URLs registered for GitHub/Google.
  • Creem — live server index, live product IDs, and the webhook (/api/payment/notify/creem) registered with CREEM_WEBHOOK_SECRET.
  • Resend — verified sending domain; correct fromEmail/supportEmail.
  • Site/metadata — set NEXT_PUBLIC_SITE_URL to your domain and update the title/description metadata in src/app/[locale]/layout.tsx (see Site Config).
  • AI keysOPENAI_API_KEY / REPLICATE_API_TOKEN if those features are enabled.
  • Storage — durable object storage configured before enabling uploads.

The AI chat and media routes are public examples with no auth, rate limiting, validation, or quotas. Add those controls before exposing them in production — see AI Chat and AI Media.

Before you call it done

  • Confirm webhook lifecycle coverage (cancel/refund/expire) if you charge users.
  • Set up database backups, log aggregation, analytics, and error reporting.
  • Plan secret rotation.

Smoke test

After deploying, verify each surface end to end:

  • Auth: register, verify email, log in, OAuth, reset password, logout.
  • Email: verification and contact messages arrive.
  • Payment: checkout creates an order; webhook marks it success.
  • Dashboard: protected routes redirect when logged out.
  • Content: docs, blog, and legal pages render.
  • Locale: / (English) and /zh routes both work.
  • AI: chat and media generation respond (if enabled).
  • Uploads: image upload works against durable storage (if enabled).

On this page