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_SECRETGITHUB_IDGITHUB_SECRETNEXTAUTH_URL(use your real public URL in production, notlocalhost)
Also required when public content is read from GitHub#
Public routes use the GitHub API when either:
NODE_ENV=production, orCMS_FORCE_GITHUB_API=trueis set (forces GitHub reads whileNODE_ENVis not production).
In that case you must also set:
GITHUB_REPO_OWNERGITHUB_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 path | Import path |
|---|---|
app/layout.tsx | '../cms/__generated__/configInit' |
src/app/layout.tsx | '../../cms/__generated__/configInit' |
| Admin catch-all page | Typical 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:
| Situation | What to do |
|---|---|
| Private repo, no token | Set CMS_GITHUB_TOKEN with access to the content repo. |
| 403 / permission errors | Ensure 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 files | Confirm 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 baseBranch | That name must exist on GitHub (heads/<name>). If it does not, resolution falls back to git.baseBranch with no error page. |
| No pointer file yet | Until 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 / network | Retry 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:
CMS_GITHUB_TOKENon Vercel (recommended for private repos and for shared remote cache), or- 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:
| Check | What to do |
|---|---|
| Repo coordinates | Set GITHUB_REPO_OWNER and GITHUB_REPO_NAME in Vercel Production (and Preview if you edit there). |
| Private repo | Set CMS_GITHUB_TOKEN to a PAT or fine-grained token with Contents: Read (and write for saves). |
| GitHub App | Install the app on the repo; grant Contents: Read and write; sign out and back in after permission changes. |
NEXTAUTH_URL | Must exactly match your deployed URL (e.g. https://octocms.gunkin.dev). |
| Base branch | git.baseBranch in cms/schema.json must match the branch on GitHub (usually main). |
| Stale deploy | Redeploy 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:
-
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. -
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. IfgetFilefell through tofs.readFileafter the in-memory store missed on a cold serverless instance,readFilethrew and Next surfaced 500. OctoCMS now retriesreadGitHubFilePublicin production and never uses local FS for missing entries in production; upgrade if you still see this on an older build. -
Production without a feature branch — Saves are rejected when the
cms-active-branchcookie 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).