Skip to content

Railway Deployment Notes

This repo uses Railway for preview and hosted runtime services under the beef project in the Foxy workspace.

Railway supports build and deploy config in railway.json or railway.toml, but in a monorepo the config file does not follow the service Root Directory automatically. Each Railway service must be configured once with both the correct Root Directory and the checked-in custom config path.

The apps/*/railway.json files are intended to be an exact mirror of the live main environment build/deploy manifest, including explicit defaulted cloud values such as watchPatterns, multiRegionConfig, replica counts, runtime flags, and healthcheck settings. If main changes, update the checked-in files from Railway before cloning the setup elsewhere.

All checked-in service manifests use us-east4-eqdc4a (US East). Brisket pins two replicas there; the other six Railway runtime services pin one. Keep production and staging in that region unless there is an explicit DR or latency decision to split them.

Service Root Directory in Railway Custom config path in Railway
beef-brain apps/brain /apps/brain/railway.json
beef-brisket repository root /apps/brisket/railway.json
beef-fennec repository root (required before deploying the shared design-system build) /apps/fennec/railway.json
beef-flank /apps/flank /apps/flank/railway.json
beef-round apps/round /apps/round/railway.json
beef-sirloin repository root /apps/sirloin/railway.json
beef-strip apps/strip /apps/strip/railway.json

For a second environment or prod-like clone, keep the following per service in sync:

  1. Service name / service type in Railway.
  2. Root Directory.
  3. Custom config path.
  4. Environment variables / secrets.
  5. Domains, volumes, and any non-JSON service attachments.

With the current repo state, Railway build/deploy settings should come from the checked-in railway.json files, so recreating the service layout is mostly a matter of pointing each service at the same Root Directory and config path. Sirloin, Brisket and Fennec use the repository root. Sirloin generates and embeds the docs KB from docs/src/content/docs/**. Brisket and Fennec compile packages/foxy-ds in a build stage and copy the result into their app images.

Root Directory is not expressible in railway.json or railway.toml — the schema covers only the build and deploy sections, while the root is a source setting. So a Dockerfile moving to a repo-root context is only half the change; the service must be repointed in Railway by hand, per environment, or every COPY of a path outside the old root fails with "/apps/<service>/<path>": not found.

Fennec’s shared design-system build requires that external Root Directory change before deployment. The checked-in configuration prepares the root-context build; it does not establish that any hosted service has been repointed.

Alpine, Wolfi, and Debian security repositories keep only the newest build of each package. An exact pin such as apk add openssl=3.5.8-r0 stops resolving the day the next patch ships, and the owning service’s next Railway build fails at that RUN step with unable to select packages (apk) or Version ... was not found (apt). The base image digest does not protect the pin, because the package index is fetched live at build time. beef-flank production failed this way on 2026-09-30.

Two rules keep this from recurring:

  1. Install an OS package only when the image uses it. The Node base images already ship the CA bundle and the TLS libraries, so a Node service rarely needs apk add at all. Flank and Brisket install nothing.
  2. Every remaining exact apk or apt pin is checked by the OS package pins workflow (.github/workflows/os-package-pins.yml), daily and on Dockerfile changes. It runs go -C tools/agent run ./cmd/check-os-pins, which simulates each install inside its pinned base image and lists every pin that no longer resolves. When it fails, bump the listed pins to the versions the repository now serves and rebuild the affected images. Add -list to print the pins without running Docker.

All Railway-tracked beef-* services are expected to have healthchecks configured.

Current healthcheck paths:

Service Healthcheck path Notes
beef-brain /health NestJS controller health endpoint
beef-brisket /api/health Next.js app route
beef-fennec / Static serve -s dist response
beef-flank /sign-in Existing app route
beef-round /health Served on the same listener as gRPC so Railway can keep PORT aligned with GRPC_PORT
beef-sirloin /health Served on the same listener as gRPC so Railway can keep PORT aligned with SIRLOIN_PORT
beef-strip /health Go SSR health route

For both round and sirloin, /health is exposed on the same socket as gRPC using HTTP/2 cleartext + HTTP routing. This avoids any extra port binding and keeps Railway’s required PORT variable aligned with the service’s primary gRPC port.

Use this from the repo root to compare the checked-in files with the live main manifest:

Terminal window
railway status --json

The serviceManifest block under each beef-* service in the main environment should match the corresponding apps/<service>/railway.json file’s build and deploy sections.

Preview environments are named beef-pr-<PR_NUMBER>.

Preview database consumers that must be rewired to Neon PR branches:

  • beef-sirloin -> SIRLOIN_DATABASE_URL and SIRLOIN_DATABASE_POOLED_URL -> Neon database rump
  • beef-brain -> DATABASE_URL and DIRECT_DATABASE_URL -> Neon database fennec with schema=fennec

The workflow at .github/workflows/neon-branching.yml creates the Neon PR branch, updates the Railway database variables, and verifies that the preview environment no longer points at railway.internal.

The GitHub workflow requires one Railway auth secret:

  • RAILWAY_TOKEN for project-scoped CI access, or
  • RAILWAY_API_TOKEN for account/workspace-scoped CI access

Without one of these secrets, preview environments will keep using their cloned Railway Postgres values.

beef-flank is currently present in the Railway project and has a checked-in config mirror at /apps/flank/railway.json.

Minimum runtime wiring for beef-flank includes:

  • BRAIN_API_URL
  • FLANK_STAGE
  • SIRLOIN_GRPC_URL (playground character search via StripService — not the removed storage/execution path)
  • CLERK_PUBLISHABLE_KEY
  • CLERK_SECRET_KEY

The legacy sirloin↔flank gRPC wiring (FLANK_ENCRYPTION_KEY, FLANK_SERVICE_TOKEN, SIRLOIN_FLANK_HOST, SIRLOIN_FLANK_SERVICE_TOKEN) has been removed; stale values can be deleted from Railway environments.

The internal docs site is built from docs/ and deployed with Cloudflare Pages.

Recommended Cloudflare Pages settings:

  • Build command: pnpm install && pnpm build
  • Build output directory: dist
  • Root directory: docs
  • Access policy: protect the Pages hostname with Cloudflare Access

Do not put secrets, live DSNs, bearer tokens, or credentials in docs.