Skip to content

Deployment

GAIA builds a React Router 8 (opens in a new tab) app with server rendering on, so it deploys wherever React Router does. GAIA does not ship a deployment workflow or pick a host for you. Ask Claude to set up deployment for the host you choose.

React Router isn’t tied to one hosting platform. You can deploy it to:

React Router’s deployment guide (opens in a new tab) covers each option.

GAIA’s app is built for server rendering: on every request, its root loader reads cookies and sets response headers. React Router can also build a single-page app that a static host serves. Switching takes more than turning off ssr in frontend/react-router.config.ts, so to use that mode, ask Claude to switch the app for you.

From the repository root, or ask Claude to build and start the production server:

Terminal window
pnpm build
pnpm start

pnpm start runs React Router’s production server (react-router-serve) on the built app. It listens on PORT, or on port 3000 (a random free port if 3000 is taken) when PORT is not set. Set HOST to choose the address it binds to. NODE_ENV defaults to production.

GAIA ships frontend/Dockerfile as a starting point. Your host may need changes to it, and Claude can make them.

Build it from the repository root, so the root lockfile and pnpm (opens in a new tab)’s supply-chain settings apply:

Terminal window
docker build -t my-app -f frontend/Dockerfile .
docker run -p 3000:3000 \
-e API_URL=https://api.example.com/ \
-e SESSION_SECRET=<a long random value> \
-e SITE_URL=https://example.com \
my-app

The image:

  • Installs dependencies from the lockfile exactly as committed, then builds the app
  • Copies only the production dependencies and the build output into a fresh Node image
  • Runs the server as the unprivileged node user, with pnpm start

No .env file ever enters the image: frontend/Dockerfile.dockerignore excludes them, along with the root .claude/ folder, the wiki, and local GAIA state.

frontend/.env is for local development only. On your host, set these variables in its own environment or secrets settings. The server checks them when it starts (frontend/app/env.server.ts) and fails to start if a required one is missing.

VariableRequiredWhat it does
API_URLYesThe base URL of your backend API, used by both the server and the browser.
SESSION_SECRETYesSigns the app’s cookies, such as the language and toast cookies. Use a long random value, never the local placeholder from .env.example.
SITE_URLYesYour site’s public URL, including the scheme (https://example.com). Pages tell search engines not to index them unless the request’s Host header matches this URL’s host and port, so a preview deployment stays out of search results. If a proxy in front of the server changes the Host header, every page is marked this way.
PORT, HOSTNoWhere the server listens.
COMMIT_SHANoSet when building to add the first six characters of the commit to the app’s version string, so you can tell which commit a running build came from (for example, when debugging). The shipped Dockerfile doesn’t pass it to the build. To include it in Docker builds, ask Claude: “Pass COMMIT_SHA into the Docker build so the app’s version string shows the commit.”
MSW_ENABLEDNoDevelopment only: starts the mock API. A production build never starts it, so leave it unset on your host.

GAIA’s Content Security Policy only lets the browser make requests to the site’s own origin (connect-src 'self'). If code running in the browser calls API_URL and your API is on another domain, the browser blocks the request. Requests made on the server, such as in loaders, are not affected. To let the browser call your API, ask Claude to add its origin to the policy:

My API is at <your API origin, such as https://api.example.com>. Allow the browser to call it in the Content Security Policy.