Docs

Railway

Deploy a Proa app to Railway from a Dockerfile in about ten minutes.

Open Markdown

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 youYour app must
A PORT environment variableRead PORT at startup. Never hardcode it.
An HTTPS edge in front of your containerBind 0.0.0.0 for public traffic or :: for dual-stack traffic, never 127.0.0.1
A health check before routing trafficReturn 200 on a cheap path such as /healthz

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

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:

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

That gives you:

Dockerfile
.dockerignore
railway.json
RAILWAY.md
src/main.rs
public/

Already have a project? Copy the Dockerfile below instead.

Adapt the Dockerfile

The generated Dockerfile installs a pinned, checksum-verified Proa CLI, builds through proa build --locked --artifact-out, and reads the GitHub token from a BuildKit secret. Railway does not support BuildKit secret mounts, so replace the --mount=type=secret block with a build argument. The builder stage is discarded, so the token never reaches the published image.

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 \
    && rm -rf /var/lib/apt/lists/*

ARG PROA_VERSION=0.1.1
RUN curl --proto '=https' --tlsv1.2 -fsSL https://proa.so/install.sh \
    | PROA_VERSION=$PROA_VERSION PROA_INSTALL_DIR=/usr/local/bin PROA_TAILWIND=yes PROA_YES=1 sh

COPY . .

# Railway injects service variables at build time only when you declare them.
# The token fetches Proa's git dependencies while the repository is private.
ARG GITHUB_TOKEN

ENV CARGO_NET_GIT_FETCH_WITH_CLI=true
RUN --mount=type=cache,id=s/<service-id>-/usr/local/cargo/registry,target=/usr/local/cargo/registry \
    --mount=type=cache,id=s/<service-id>-/usr/local/cargo/git,target=/usr/local/cargo/git \
    --mount=type=cache,id=s/<service-id>-/app/target,target=/app/target \
    set -eu; \
    export GIT_CONFIG_COUNT=1; \
    export GIT_CONFIG_KEY_0=credential.https://github.com.helper; \
    export GIT_CONFIG_VALUE_0='!f() { if [ "$1" = get ] && [ -n "$GITHUB_TOKEN" ]; then printf "username=x-access-token\npassword=%s\n" "$GITHUB_TOKEN"; fi; }; f'; \
    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"]

Three changes from the generated version:

--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 <service-id> 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:

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:

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:

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:

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 makes.

Verify before you deploy

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

SymptomCause
Health check times out, logs look fineApp bound 127.0.0.1, or ignored PORT
Build succeeds, service unreachableNo domain generated under Networking
Authentication failure fetching Proa git dependenciesARG 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 environmentPROA_HOST=0.0.0.0; legacy IPv6-only private networking needs ::
Every push rebuilds from scratchNo cache mounts, or .dockerignore omits target/

Next steps

Search

Type at least 2 characters