Production Checklist
What to check before a GioJS app takes real traffic - and what you can leave alone because the defaults already got it right.
GioJS starts with production-safe defaults: no error details in responses, security headers on every response, cross-site form posts refused, dev endpoints off, metrics off. Most of this list is about the parts only you can know: your secrets, your proxy, your traffic. Each item links to the page with the details.
Runtime mode
- Start in production mode. Anything but
NODE_ENV=developmentis production -npm startand a standalonerun.mjsdefault to it,npm run devdoes not. The server decides once and starts its workers in the same mode. - Check an error page. In production a failed render shows only an error reference (digest); the message and stack go to the log under the same digest. If you see a stack trace in the browser, the server is in development mode. See Error Handling.
- Use Node 20 or newer, and keep
@gio.js/server,@gio.js/coreand@gio.js/reacton the same version - they are released in lockstep. - Check where fonts come from. A
[[fonts]]file inpublic/(the starter's) is copied at every start and needs no network - make sure it is in the image or standalone folder, since a missing file stops startup. Anhttps://fonturlis downloaded into.gio/fonts/on a fresh host's first start, and a failed download stops startup: allow outbound HTTPS to that URL, persist the folder, or move the file intopublic/. See Font Optimization.
Secrets and environment
- Set
GIO_SESSION_SECRET(32+ bytes) if you use sessions orrequire_sessionguards. Without it,createSessionStorage()throws, so every page androute.tsimporting your session module answers 500 (the log names the file and the missing secret under the response's digest), and guards deny every request. The server still starts - checknpx gio routeswith the production environment for routes marked(failed to load). - Set
GIO_REVALIDATE_TOKEN(32+ bytes) only if a CMS or script callsPOST /_gio/revalidate; without it the endpoint does not exist. - Keep secrets out of the browser. Nothing secret is named
GIO_PUBLIC_*,getServerSidePropsreturns only what the page shows (props are sent to the browser), and modules holding secrets import@gio.js/core/server-only. - Keep secrets out of git:
.env*.localis ignored, and production values live in the host's environment. See Environment Variables.
Security headers and CSP
- Defaults are on:
X-Content-Type-Options,X-Frame-Options: SAMEORIGIN,Referrer-Policy. AddPermissions-Policyand the cross-origin policies your app can live with in[security.headers]. - Add a Content-Security-Policy. Start with
csp_report_only, watch the browser console on every page, then switch tocsp. Nonces are fresh per response, cache hits included; nonce your own inline and third-party scripts withcspNonce(). See the Content Security Policy guide. - HSTS. GioJS sends
Strict-Transport-Securityon its own only when it terminates TLS. Behind a TLS proxy or a platform, set[security] hsts = trueonce the whole site is HTTPS.
toml
[security]
csp_report_only = "default-src 'self'; script-src 'self' 'nonce-{nonce}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
hsts = true
[security.headers]
permissions-policy = "camera=(), microphone=(), geolocation=()"CSRF and origins
- Same-origin forms and
fetchcalls just work; cross-sitePOST/PUT/PATCH/DELETErequests and WebSocket upgrades get a 403 before Node sees them. - List other origins of yours that post to this app (an admin subdomain, a marketing site) in
[security.csrf] trusted_origins. - Exempt cross-site callbacks - OAuth/OIDC
response_mode=form_post, SAML ACS, payment-provider returns, webhooks that send anOrigin- in[security.csrf] exempt, and make sure each verifies its own signature or state. See CSRF protection. - Keep state changes off
GET, and session cookiesSameSite=Lax(the default) orStrict.
Reverse proxy and client IPs
- Trust exactly your proxy in
[server] trusted_proxies, so rate limits, the metrics allowlist andreq.ipsee visitors instead of the proxy. Leave it empty when GioJS faces the internet directly. - Make GioJS unreachable around the proxy: bind
host = "127.0.0.1"or firewall the port. - Pass the original
Hostthrough - the CSRF and WebSocket checks compare the browser'sOriginwith it. - Decide who sets
X-Request-Id: let the proxy set it, oraccept_request_id = falseif the proxy passes a client's through. - Line up keep-alive timeouts with a proxy that pools connections, or it can answer with sporadic 502s; turn off response buffering (nginx
proxy_buffering off) so streamed pages stream. See Behind a reverse proxy.
Limits
- Rate-limit what attackers hammer: login and signup actions, password resets, expensive APIs.
[[rate_limits]]run in Rust before routing. See Rate limits. - Size request bodies:
[server] max_body_bytes(2 MiB by default) caps uploads - raise it for file uploads, and match the proxy's limit (nginxclient_max_body_size). - Keep the file-descriptor limit (
ulimit -n,LimitNOFILE=in systemd) above[server] max_connections(10000). See Connection limits. - Allow only the image hosts you use in
[[images.remote_patterns]], with apathnamewhere you can.
Caching
- Cache what can be shared. Pages without
export const revalidaterender on every request. Check what each route does withgio cache explain <url>or theX-Gio-Cacheheader. Personalized pages are never cached - see Caching & Revalidating. - Size the cache:
[cache] memory_max_entries(1000 pages) anddisk_max_bytes(512 MiB). Putdisk_path(orGIO_CACHE_DIR) on a persistent volume if a restart should come back warm. - Purge on change with
revalidateTag()/revalidatePath()or a CMS webhook to/_gio/revalidate. The cache is per instance: with several instances, purge each one. - Several instances of one release: set
GIO_DEPLOYMENT_IDto the same value (the release SHA) on all of them. See Multi-instance deployments.
Render workers
- Pick a worker count. One worker is plenty when most traffic is cache hits and static files. When uncached renders queue up, set
[server] workersto a number or"auto". - Budget memory per worker - each is a full Node process (often 100-200 MB) - and set container limits for the whole pool.
- Run one-time jobs once: plugin
onStartupruns in every worker; guard migrations and schedulers withprocess.env.GIO_WORKER_INDEX === '0'or move them out of the server. See Sizing.
Logs, metrics and health
- JSON logs for a log shipper:
GIO_LOG_FORMAT=json(or[logging] format = "json"). Every line of a request carries its request id in both processes. See Observability. - Metrics:
[metrics] enabled = truewith anip_allowlistortoken- without either it answers only this machine, and an allowlist of0.0.0.0/0with no token logs a warning at startup. Alert on 5xx rates perroute, p95 latency, and a climbinggio_worker_restarts_total. - Health checks on
/_gio/health. It always answers 200 while the server runs; readnodeReadyfor "can render right now".[health] details = falsekeeps the deployment id and worker counts out of it;[health] enabled = falseturns it into a404, so point every probe at a page of your own first. See Health check. - Graceful stops: stop with
SIGTERMand allow at least 15-20 seconds (requests drain for up to 8, then workers get a few more). Several defaults are shorter: Docker's 10 seconds (use--stop-timeout 20), Fly.io's 5 (kill_timeout = 20) and Railway's 0 ("drainingSeconds": 20) - see Deploying. In systemd useKillMode=mixed.
SEO and URLs
- Set
GIO_SITE_URL(ormetadataBase) so canonical and Open Graph URLs,sitemap.xmlandrobots.txtare absolute. See Metadata & SEO. - Never derive URLs or security decisions from
req.host- the host header is client-supplied.
Before every release
npm run build # typecheck (tsc --noEmit) - normal deploys have no other build step
npm test # your tests - @gio.js/core/testing has renderPage, callRoute, createTestServer
npx gio build standalone # if you ship a standalone folder or imagepnpm build # typecheck (tsc --noEmit) - normal deploys have no other build step
pnpm test # your tests - @gio.js/core/testing has renderPage, callRoute, createTestServer
pnpm exec gio build standalone # if you ship a standalone folder or imageyarn build # typecheck (tsc --noEmit) - normal deploys have no other build step
yarn test # your tests - @gio.js/core/testing has renderPage, callRoute, createTestServer
yarn gio build standalone # if you ship a standalone folder or imagebun run build # typecheck (tsc --noEmit) - normal deploys have no other build step
bun run test # your tests - @gio.js/core/testing has renderPage, callRoute, createTestServer
bunx gio build standalone # if you ship a standalone folder or imageThen deploy with one of the deployment recipes, and keep an eye on known limitations when planning features.