Docs
Railway
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:
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.dockerignorerailway.jsonRAILWAY.mdsrc/main.rspublic/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.
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:
ARGreplaces the BuildKit secret mount.PROA_HOST=::replacesPROA_HOST=0.0.0.0, so private networking works.- No
ENV PORT, noEXPOSE. Railway setsPORTand 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 <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:
{
"$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:
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
| 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
- Pick a deployment target, then follow the guide for it.
- Environment variables
- Configure a deployed Proa app without rebuilding the binary.
- CI / GitHub Actions
- Gate pull requests with proa check, then deploy from the same pipeline.
- Coolify
- Deploy a Proa app to Coolify on your own VPS.