Show navigationHide navigation

Deployment errors and troubleshooting

This page describes how the CMS fails fast when configuration is wrong, and how to fix common GitHub-related errors on the public site.

Environment validation#

When NODE_ENV is production, the app checks required variables while loading next.config.ts and again when the Node server starts (next start, including Vercel).

If something is missing, next build or next start stops with an error like:

Missing required environment variables for production: … See README.md (Production requirements) and docs/deployment-errors.md.

Always required in production#

  • NEXTAUTH_SECRET
  • GITHUB_ID
  • GITHUB_SECRET
  • NEXTAUTH_URL (use your real public URL in production, not localhost)

Also required when public content is read from GitHub#

Public routes use the GitHub API when either:

  • NODE_ENV=production, or
  • CMS_FORCE_GITHUB_API=true is set (forces GitHub reads while NODE_ENV is not production).

In that case you must also set:

  • GITHUB_REPO_OWNER
  • GITHUB_REPO_NAME

For private repositories, set CMS_GITHUB_TOKEN with Contents: Read (and write access for CMS saves). See Installation.

During Vercel next build, public query() reads content from the cloned git checkout on disk (the checked-out branch, usually main) instead of the GitHub API. Serverless runtime still uses the GitHub API with per-build pointer resolution. A valid CMS_GITHUB_TOKEN remains required for live traffic on private repos.

Auth routes require the Node.js runtime (default on Vercel). Do not set runtime = 'edge' on /api/octocms/* re-exports — OAuth token exchange and Octokit need Node.

"OctoCMS config not initialized" (500 on public pages)#

Error message:

Error: OctoCMS config not initialized. Make sure withOctoCMS(nextConfig, config) is called
in next.config.ts and cms/__generated__/configInit.ts is imported in your admin layout.

Cause: OctoCMS stores its runtime config in a module-level singleton (octocms/lib/configStore). In serverless deployments each route runs in its own function instance. If a public page cold-starts without having loaded configInit, the singleton is empty and every query() call throws.

The admin layout (app/cms/layout.tsx) already imports configInit, so /cms/* routes are fine. Public routes are affected only when they cold-start in a separate function instance that never loaded the admin layout.

Fix: Import configInit in your root layout so it runs for every route, and keep the admin catch-all page (app/cms/[[...path]]/page.tsx or src/app/cms/[[...path]]/page.tsx) side-effect-importing the same module. Some Next.js server action bundles do not load layout.tsx for the POST that carries the action; without the catch-all import, getConfig() can throw even when /cms works in the browser.

typescript
// app/layout.tsx  (or src/app/layout.tsx)
import '../cms/__generated__/configInit';   // ← add this line

The relative path depends on where your root layout lives:

Root layout pathImport path
app/layout.tsx'../cms/__generated__/configInit'
src/app/layout.tsx'../../cms/__generated__/configInit'
Admin catch-all pageTypical import path
app/cms/[[...path]]/page.tsx'../../../cms/__generated__/configInit'
src/app/cms/[[...path]]/page.tsx'../../../../cms/__generated__/configInit'

If you used npx octocms init, the CLI wires the catch-all page and adds the cms/__generated__/* TypeScript path. Run npx octocms update to refresh admin route stubs on an existing project (it migrates legacy one-line re-exports).

TypeScript path: Admin server actions side-import cms/__generated__/configInit via octocms/admin/actions/registerConfig.ts. Your app’s tsconfig.json must map that specifier (for example "cms/__generated__/*": ["./cms/__generated__/*"]). Without it, next build can fail to resolve the import even though relative imports in layouts work.

Public site: content could not be loaded#

If GitHub is unreachable, rate-limited, or the token cannot read the repo, the App Router shows a “Something went wrong” page with a short explanation and Try again / Home.

Typical causes:

SituationWhat to do
Private repo, no tokenSet CMS_GITHUB_TOKEN with access to the content repo.
403 / permission errorsEnsure the token or app installation has Contents: Read on the repository.
Rate limit (429)Wait and retry; reduce traffic or use a token for higher limits.
Wrong branch / missing filesConfirm git.baseBranch and optional git.publishedPointerBranch in your config match your repo; ensure per-build pointer files under cms/pointers/ (and CMS_BRANCH if used) and content paths exist on the ref you read. See docs/multi-deploy.md.
CMS_BRANCH set but site still uses baseBranchThat name must exist on GitHub (heads/<name>). If it does not, resolution falls back to git.baseBranch with no error page.
No pointer file yetUntil you Publish once, switch the header branch off the default in production, or add cms/pointers/<build-id>.json on the pointer ref, public reads use CMS_BRANCH when set and valid, otherwise git.baseBranch. See Multi-deploy.
GitHub outage / networkRetry later; check GitHub Status.

CMS admin: /cms shows "GitHub could not be reached" or 504#

What you see: The yellow banner on /cms says content is unavailable, the entry list stays empty, or Vercel returns 504 on the first load after a cold start.

Cause: In production the admin UI loads content from the GitHub API. Public query() reads and admin reads both need repository access. For private repositories, unauthenticated GitHub API calls return 404 (GitHub hides private repos). OctoCMS therefore needs either:

  1. CMS_GITHUB_TOKEN on Vercel (recommended for private repos and for shared remote cache), or
  2. A signed-in CMS user whose GitHub App installation has Contents: Read on the repository (OctoCMS falls back to the OAuth session token for admin reads when no static token is set).

Checklist:

CheckWhat to do
Repo coordinatesSet GITHUB_REPO_OWNER and GITHUB_REPO_NAME in Vercel Production (and Preview if you edit there).
Private repoSet CMS_GITHUB_TOKEN to a PAT or fine-grained token with Contents: Read (and write for saves).
GitHub AppInstall the app on the repo; grant Contents: Read and write; sign out and back in after permission changes.
NEXTAUTH_URLMust exactly match your deployed URL (e.g. https://octocms.gunkin.dev).
Base branchgit.baseBranch in cms/schema.json must match the branch on GitHub (usually main).
Stale deployRedeploy after changing environment variables.

504 specifically: The first /cms visit after a cold start may fetch the full content index from GitHub. On the Hobby plan the default function timeout is 60 s. If loading still times out with a valid token, check Vercel → Logs for contentStore.fetch errors and confirm the token can read cms/content/ on the active branch.

CMS admin: POST /cms/content/... returns 500 after Save#

What you see: The browser Network tab shows 500 on a POST to the same URL as the entry editor (for example /cms/content/homePage/homePage-0000). That request carries the Server Action payload (save plus any refetches), not a missing route.

Common causes:

  1. GitHub write failure — Missing or under-scoped installation token, branch protection, or wrong branch. The action usually returns { success: false, error: "…" } with a toast; only uncaught errors become 500. Check Vercel Functions logs for the stack trace.

  2. Refetch after save hit a code path that read the repo from local disk — On Vercel the deployed app does not ship your Git working tree under process.cwd() the way dev does. If getFile fell through to fs.readFile after the in-memory store missed on a cold serverless instance, readFile threw and Next surfaced 500. OctoCMS now retries readGitHubFilePublic in production and never uses local FS for missing entries in production; upgrade if you still see this on an older build.

  3. Production without a feature branch — Saves are rejected when the cms-active-branch cookie is unset (Create or select a branch before editing.). That path returns a structured failure, not 500, unless something else throws afterward.

What to do: Open Vercel → Project → Logs (or Runtime Logs) for the failing timestamp and read the server error message. Confirm CMS_GITHUB_TOKEN / GitHub App permissions, that you are on a writable CMS branch from the header, and that configInit is imported from the root layout (see above).