Build config
The railway.json reference — builders, start command, migrations, healthchecks — and how to use a Dockerfile when you need system packages.
Your repository controls how it builds and runs through a railway.json file at its root. Buildspace reads it on every deploy — there is no separate build configuration stored on the platform.
railway.json reference
The starter ships this:
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "RAILPACK"
},
"deploy": {
"startCommand": "bun run start",
"preDeployCommand": "bun run db:migrate",
"healthcheckPath": "/api/health",
"healthcheckTimeout": 100,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}| Field | What it does |
|---|---|
build.builder | How the image is built: RAILPACK (default, zero-config) or DOCKERFILE (you provide a Dockerfile). |
deploy.startCommand | How the container starts. Overrides a Dockerfile's CMD. |
deploy.preDeployCommand | Runs once against the environment before the new release takes traffic — the starter runs Drizzle migrations here so schema changes ship with the code. |
deploy.healthcheckPath | Railway polls this path; the release only goes live once it returns 200. |
deploy.restartPolicy* | What to do if the container crashes. |
The deploy.* fields are builder-independent — startCommand, preDeployCommand, and the healthcheck behave identically whether you build with Railpack or a Dockerfile.
Railpack (the default)
Railpack auto-detects a Bun + Next.js app and builds it with no configuration. Keep it unless you specifically need OS-level packages. It handles the starter's standalone output (bun run build → .next/standalone) out of the box.
Using a Dockerfile
Switch to a Dockerfile when your app needs system packages Railpack can't install — ffmpeg for video, Chromium/Playwright for headless browsers, ImageMagick, extra fonts, and so on. This is fully supported; the deploy pipeline does not assume Railpack.
Two steps:
- Add a
Dockerfileat the repo root. The starter ships a known-good one asDockerfile.example— rename it toDockerfile. - Point
railway.jsonat it:
{
"build": { "builder": "DOCKERFILE" }
}Leave the deploy block as-is: your migrations and healthcheck keep working.
Known-good starter Dockerfile
A multi-stage build for the starter's Bun + Next.js standalone layout. The runtime stage carries drizzle-kit so the preDeployCommand migration runs, and reproduces the .next/standalone tree so bun run start behaves exactly as it does under Railpack:
# syntax=docker/dockerfile:1
FROM oven/bun:1 AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
FROM oven/bun:1 AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# NEXT_PUBLIC_* are inlined at build time; SKIP_ENV_VALIDATION lets the build
# run without the server-only secrets (injected at runtime instead).
ARG NEXT_PUBLIC_BUILDSPACE_PUBLISHABLE_KEY
ENV NEXT_PUBLIC_BUILDSPACE_PUBLISHABLE_KEY=$NEXT_PUBLIC_BUILDSPACE_PUBLISHABLE_KEY
ENV SKIP_ENV_VALIDATION=1
ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run build
FROM oven/bun:1 AS prod-deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
FROM oven/bun:1 AS runtime
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
# --- System packages: the reason to use a Dockerfile at all ---
# RUN apt-get update \
# && apt-get install -y --no-install-recommends ffmpeg \
# && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/.next/standalone ./.next/standalone
COPY --from=prod-deps /app/node_modules ./node_modules
COPY package.json drizzle.config.ts ./
COPY drizzle ./drizzle
COPY lib ./lib
EXPOSE 3000
CMD ["bun", "run", "start"]Build-time vs. runtime environment variables
This trips people up when moving from Railpack to a Dockerfile:
NEXT_PUBLIC_*variables are inlined into the client bundle at build time. They must exist while the image builds. Railway supplies matching service variables as build args when you declare them withARGin the Dockerfile (as above). Miss this and the browser getsundefinedfor your publishable key.- Server-only secrets (
BUILDSPACE_SECRET_KEY,BUILDSPACE_DB_URL,BUILDSPACE_DB_TOKEN, add-on credentials) are injected at runtime. Do not bake them into the image.SKIP_ENV_VALIDATION=1lets the build proceed without them.
Railpack handles this split for you; with a Dockerfile you declare it explicitly.
Verifying locally
You can build and run the image before deploying:
docker build -f Dockerfile \
--build-arg NEXT_PUBLIC_BUILDSPACE_PUBLISHABLE_KEY=bs_pub_local \
-t my-app .
docker run --rm -p 3000:3000 \
-e BUILDSPACE_SECRET_KEY=bs_sec_local \
my-app
# then: curl localhost:3000/api/health -> {"ok":true,"db":...}To exercise the pre-deploy migration the way Railway does, run it against the image explicitly:
docker run --rm -e BUILDSPACE_DB_URL=... -e BUILDSPACE_DB_TOKEN=... my-app bun run db:migrate