Railway is a Git-driven PaaS that builds your
Dockerfile and runs it on managed infrastructure. Pair it with Railway's
managed Postgres and Redis and you have a complete Suprnova production
stack with no servers to babysit. This recipe takes a freshly scaffolded
app from suprnova new to a live URL.
Prerequisites
- A Railway account
- A Suprnova project pushed to GitHub, GitLab, or Bitbucket
- A
Dockerfileand.dockerignoreat the repo root, generated by: - A generated
APP_KEYyou can paste into Railway's variables:
suprnova is only needed locally - Railway builds the Dockerfile itself.
The framework crate is pulled from git as a normal cargo dependency
during the build.
Provision the project
- Open the Railway dashboard, click New Project, and choose Deploy from GitHub repo.
- Pick the repository. Railway detects the
Dockerfileand starts the first build automatically. - While it's building, add a database: New → Database →
Add PostgreSQL. Railway exposes
DATABASE_URLas a reference variable on the project. - Optionally add Redis the same way (New → Database → Redis)
if your app uses the Redis cache, session, queue, or rate-limit
driver. Railway exposes the connection URL as
REDIS_URL.
Wire the variables
Open the web service, go to Variables, and add the production
configuration. Use Railway's ${{ }} reference syntax to pull URLs
from the database services so rotations don't require re-pasting.
APP_ENV=production
APP_KEY=<paste the output of `suprnova key:generate --show`>
SERVER_HOST=0.0.0.0
SERVER_PORT=8765
DATABASE_URL=${{ Postgres.DATABASE_URL }}
REDIS_URL=${{ Redis.REDIS_URL }}
A few things worth knowing:
APP_KEYis mandatory in non-development environments. Suprnova fails closed on boot whenAPP_ENV != local|dev|testandAPP_KEYis missing or malformed. The server logs a remediation message and exits non-zero - Railway will mark the deploy failed. Generate the key withsuprnova key:generate --show.SERVER_HOST=0.0.0.0is required. Railway routes traffic through the container's network interface; binding to127.0.0.1(the local default) will look like a refused connection.SERVER_PORTmatchesEXPOSEin the Dockerfile. The generated Dockerfile exposes 8765. Railway maps it to a public URL automatically.
Build and deploy
Railway builds on every push to the connected branch. The Dockerfile
generated by docker:init does:
- Stage 1 - Frontend. Runs
npm ciandnpm run buildinfrontend/. The Vite output lands infrontend/dist/. - Stage 2 - Backend. Runs
cargo build --releaseagainst your workspace; cached dependency layers keep iterative builds fast. - Stage 3 - Runtime. A
debian:bookworm-slimimage withca-certificates+libssl3, a non-rootappuser, and the compiled./appbinary. DefaultCMDis./app, which runsservewith auto-migrate.
The first build typically takes several minutes (cold Rust cache); follow-up builds are much faster thanks to Docker layer caching.
Add a scheduler service
If your app uses #[derive(Task)] schedules, the scheduler needs its
own long-running process. Add a second service from the same repo:
- New → GitHub Repo → pick the same repository.
- Name it
schedulerso it's easy to spot in the dashboard. - Under Settings → Deploy set the Custom Start Command to:
- Copy the same variables (especially
APP_KEYand the database references) so the worker reads the same configuration as the web service.
schedule:work is a daemon loop - it wakes once a minute, queries the
schedule for due tasks, and runs them through the same bootstrap as the
HTTP server. See Console and the scheduler chapter for the
contract.
Run exactly one scheduler instance. Multiple schedule:work processes
coordinate via cache-backed locks, but the default expectation is a
single worker.
Why Suprnova diverges
A Laravel deploy on Forge or Vapor typically wires a webserver
(php-fpm + nginx), a queue worker (php artisan queue:work), and a
cron entry that invokes schedule:run every minute. Three components,
three deployment surfaces.
Suprnova compiles every role into the same binary. The Railway service
spec is ./app for the web role and ./app schedule:work for the
scheduler - same image, same bootstrap, different argv. There is no
separate php-fpm container, no separate worker image, no host cron. Add
./app queue:work as a third service if you have queued jobs and you
have the full Laravel topology in three Railway services from one
Dockerfile.
Health checks and railway.json
For more control over the deploy, commit a railway.json to the repo
root. Railway picks it up automatically.
Suprnova ships built-in health endpoints that short-circuit before the
middleware chain - they return a 200 JSON status without going through
auth, CSRF, or rate-limiting. The /_suprnova/ prefix is reserved so
they never collide with your routes.
healthcheckPath above points at /_suprnova/health/live, which
touches nothing. That pairing is deliberate: this service is configured
"restartPolicyType": "ON_FAILURE", so whatever the health check probes
is a restart trigger. Pointing it at the database - via
/_suprnova/health/ready or the older /_suprnova/health?db=true -
means a database blip restarts every replica at the moment the database
can least afford a reconnect storm. Probe the database from a separate
readiness check or your monitoring, not from the path that restarts the
process. See Use the right probe for the right
question.
Both older paths keep working, so an existing Railway service needs no change; the named paths are simply clearer.
Custom domains and TLS
- In the web service, open Settings → Networking.
- Click Generate Domain for a
*.up.railway.appsubdomain, or Custom Domain to point your own hostname at the service. - Update DNS as Railway instructs (a
CNAMEfor subdomains, an ANAME/ALIAS for apex domains).
Railway provisions and renews Let's Encrypt certificates for both generated and custom domains.
Migrations in CI/CD
The default CMD ["./app"] runs migrations on boot, which is fine for
single-instance deploys. For multi-replica setups, decouple the
migration step:
- Add a one-shot pre-deploy hook that runs
./app migrateagainst the production database before the new replicas start. - Change the runtime start command to
./app serve --no-migrateso the replicas don't race each other.
The migration runner is idempotent - even if you don't split the steps, running migrations on every boot is safe across replicas. The split exists so you can fail the deploy early on a bad migration without holding the rollout open.
Logs, metrics, rollbacks
The web service tab exposes:
- Deployments - every build in chronological order; the three-dot menu on a previous successful deploy is the one-click rollback path
- Logs -
tracingoutput from the container, with structured-log fields (request_id,route,status) ready for the log viewer's filters - Metrics - CPU, memory, network IO; useful for sizing the instance up or down
Troubleshooting
Build fails on cargo build --release. Reproduce locally with
docker build -t myapp .. The most common cause is a workspace member
that compiles on your machine but is missing from the repo - the
Dockerfile copies Cargo.toml and Cargo.lock first, so missing crates
fail loudly.
App returns "connection refused". Check SERVER_HOST=0.0.0.0 is
set on the service. The default is 127.0.0.1, which Railway can't
route to.
App boots then exits with a key error. APP_KEY is unset or
malformed. The framework refuses to boot in production without one;
re-paste the output of suprnova key:generate --show into the
service's variables.
Migrations fail on boot. Check the logs for the underlying SQL
error. Common causes are an unset DATABASE_URL (verify the
${{ Postgres.DATABASE_URL }} reference resolved) or a migration that
ran against a stale baseline (./app migrate:status reports what's
applied where).
Scheduler never fires. Verify the start command is exactly
./app schedule:work (not schedule:run, which runs due tasks once
and exits). schedule:list from a one-shot deploy confirms your tasks
are registered.
Next
- Deployment Overview - the unified-binary model your Railway services run
- Docker CLI - what
docker:initanddocker:composeactually generate - Configuration -
.envloading, typed config, required keys - Console -
schedule:work,queue:work,workflow:work, and the rest of the unified CLI - Deploy to Digital Ocean - the same recipe on a different PaaS
