---
title: Railway
description: Deploy a Proa app to Railway from a Dockerfile in about ten minutes.
---

Railway builds your Dockerfile, injects a `PORT`, waits for a health check, then swaps traffic to the new container. Proa apps are ordinary Rust HTTP servers, so nothing about Proa needs special handling: compile the binary in a build stage, run it in a slim runtime stage.

Ten minutes from an existing project to a public URL.

## What Railway needs from your app

Three things. Everything else is default.

| Railway gives you | Your app must |
|---|---|
| A `PORT` environment variable | Read `PORT` at startup. Never hardcode it. |
| An HTTPS edge in front of your container | Bind `0.0.0.0` for public traffic or `::` for dual-stack traffic, never `127.0.0.1` |
| A health check before routing traffic | Return `200` on a cheap path such as `/healthz` |

Scaffolded Proa sites already do all three. If you are wiring a server by hand:

```rust filename="src/main.rs"
use std::net::SocketAddr;

let port: u16 = std::env::var("PORT")
    .ok()
    .and_then(|value| value.parse().ok())
    .unwrap_or(3000);

// Railway recommends [::] when the service needs both public and private
// networking. New environments are dual-stack; legacy private networks are
// IPv6-only.
let addr = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], port));
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
```

## Generate the Dockerfile

Proa's Railway preset writes the Dockerfile and the Railway config for you:

```bash
proa new site my-site --template marketing --deploy railway --tailwind --yes
cd my-site
```

That gives you:

```proa-filetree
Dockerfile
.dockerignore
railway.json
RAILWAY.md
src/main.rs
public/
```

If the generated Dockerfile's pinned binary archive is unavailable, use the
Cargo-based example below. For an existing scaffolded project, add it at the
root beside `proa.lock.json`.

## Adapt the Dockerfile

This Dockerfile reads the CLI version from your project's `proa.lock.json`,
compiles that pinned version from crates.io, and builds the app
through `proa build --locked --artifact-out`. The first CLI compilation takes
several minutes; Docker caches that layer separately from your application
source. The installer handles only Tailwind and verifies its standalone binary's
checksum. Proa dependencies resolve from crates.io without credentials.
Railway's cache mounts reuse downloaded crates and compiled dependencies:

```dockerfile filename="Dockerfile"
FROM rust:1.93-bookworm AS build
WORKDIR /app
RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl git jq \
    && rm -rf /var/lib/apt/lists/*

COPY proa.lock.json /tmp/proa.lock.json
RUN --mount=type=cache,id=s/<service-id>-/usr/local/cargo/registry,target=/usr/local/cargo/registry \
    proa_version="$(jq -er '.scaffold.cliVersion' /tmp/proa.lock.json)" \
    && cargo install proa-cli --version "=$proa_version" --locked

# Install the checksum-verified standalone Tailwind CLI.
RUN curl --proto '=https' --tlsv1.2 -fsSL https://proa.so/install.sh \
    | sh -s -- --tailwind-only --to /usr/local/bin --no-modify-path --yes

COPY . .

RUN --mount=type=cache,id=s/<service-id>-/usr/local/cargo/registry,target=/usr/local/cargo/registry \
    --mount=type=cache,id=s/<service-id>-/app/target,target=/app/target \
    proa build --locked --artifact-out /app/my-site

FROM debian:bookworm-slim AS runtime
WORKDIR /app
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/my-site /usr/local/bin/my-site
COPY --from=build /app/public ./public

ENV PROA_HOST=::
ENV RUST_LOG=info

CMD ["my-site"]
```

The Railway-specific settings are:

- **Cache mounts** reuse Cargo's registry downloads and build output.
- **`PROA_HOST=::`** replaces `PROA_HOST=0.0.0.0`, so private networking works.
- **No `ENV PORT`, no `EXPOSE`.** Railway sets `PORT` and routes to it.

`--artifact-out /app/my-site` matters: cache mounts do not persist into the final layer, so the binary must land outside `/app/target`. The generated server serves `public/` relative to its working directory, so keep `WORKDIR /app` and copy `public/` beside the binary.

Replace every `<service-id>` placeholder with the service ID shown by
`railway status --json`.
Railway requires that literal `s/<service-id>-` cache-key prefix and does not
allow variables in cache-mount IDs.

Railway detects a file named `Dockerfile` at the repository root. For a nested
Dockerfile, set `dockerfilePath` in `railway.json` (or set
`RAILWAY_DOCKERFILE_PATH` in the dashboard).

## Add railway.json

Config as code keeps deploy settings in review instead of in a dashboard. The preset writes this file; `railway.toml` with the same keys also works:

```json filename="railway.json"
{
  "$schema": "https://railway.com/railway.schema.json",
  "deploy": {
    "healthcheckPath": "/readyz",
    "healthcheckTimeout": 120,
    "restartPolicyType": "ON_FAILURE"
  }
}
```

The generated site answers `/healthz` (liveness) and `/readyz` (readiness, which also checks the database when one is configured). Rust release builds are slow on a cold cache; raise `healthcheckTimeout` toward 300 seconds if the first deploy times out before the server is up.

## Deploy

From the CLI:

```bash
npm i -g @railway/cli
railway login
railway link
railway up
```

Or connect the GitHub repository in the Railway dashboard and push. Railway builds on every commit to the linked branch.

Then open **Settings → Networking → Generate Domain** to get a public URL. Without this step the service runs but nothing reaches it.

Under **Variables**, add:

```dotenv
GITHUB_TOKEN=<a token with read access to Proa-Labs/proa>
RUST_LOG=info
```

Do not set `PORT` or `PROA_HOST`. Railway manages `PORT`, and the Dockerfile sets `PROA_HOST`.

## Health checks

Railway calls `healthcheckPath` once per deploy and waits for a `200` before routing traffic to the new container. It does not poll afterwards.

Keep the liveness route cheap:

```rust filename="src/main.rs"
async fn healthz() -> &'static str {
    "ok\n"
}
```

Checking your database here means a slow database fails your deploy. Put dependency checks on a separate `/readyz` route with `proa_framework_axum::Readiness`, which probes fresh on every request and recovers without a restart.

Railway sends the `Host` header `healthcheck.railway.app`. If you add host filtering later, allowlist it.

> **Good to know**: Building Rust on every push costs minutes and puts your GitHub token on Railway. Build and push to GHCR in GitHub Actions instead, then create the Railway service from a Docker image and remove `GITHUB_TOKEN`. This is the same argument the [Coolify guide](/docs/framework/coolify) makes.

## Verify before you deploy

```bash
docker build --build-arg GITHUB_TOKEN=$GITHUB_TOKEN -t my-site .
docker run --rm -p 3000:3000 -e PORT=3000 my-site
curl -fsS http://127.0.0.1:3000/healthz
curl -fsS http://127.0.0.1:3000/static/styles.css -o /dev/null
docker run --rm my-site env | grep -c GITHUB_TOKEN   # expect 0
```

## When it fails

| Symptom | Cause |
|---|---|
| Health check times out, logs look fine | App bound `127.0.0.1`, or ignored `PORT` |
| Build succeeds, service unreachable | No domain generated under Networking |
| Authentication failure fetching Proa git dependencies | `ARG GITHUB_TOKEN` missing from the build stage |
| Binary missing in runtime stage | `--artifact-out` pointed inside the cache-mounted `target/` |
| Another service cannot reach this one in a legacy environment | `PROA_HOST=0.0.0.0`; legacy IPv6-only private networking needs `::` |
| Every push rebuilds from scratch | No cache mounts, or `.dockerignore` omits `target/` |

## Next steps

- [Deploy overview](/docs/framework/deployment)
  - Pick a deployment target, then follow the guide for it.
- [Environment variables](/docs/framework/environment)
  - Configure a deployed Proa app without rebuilding the binary.
- [CI / GitHub Actions](/docs/guides/ci)
  - Gate pull requests with proa check, then deploy from the same pipeline.
- [Coolify](/docs/framework/coolify)
  - Deploy a Proa app to Coolify on your own VPS.
