Deploy a Rails App to Railway and Get Preview Environments for Every Pull Request
When you open a pull request, you want a preview environment where reviewers can immediately verify changes. Railway offers PR Preview Environments as a built-in feature, automatically creating and destroying isolated environments (including databases) for each Pull Request (PR). No GitHub Actions workflows required.
I deployed a minimal Rails 8.1 + PostgreSQL app to Railway and verified that PR preview environments work end to end.
- GitHub: rails-preview-demo
What is Railway
Railway is a cloud platform that simplifies application deployment and infrastructure management.
| Feature | Description |
|---|---|
| Auto Build | Automatically detects build method from source code (Dockerfile, Nixpacks) |
| PR Preview | Automatically creates and destroys isolated environments per PR |
| Visual Canvas | Graphically manage connections between services |
| Built-in Databases | Add PostgreSQL, MySQL, Redis, MongoDB with one click |
| Auto Scaling | CPU/RAM scaling and replica management |
| Pay-as-you-go | Pay only for what you use. Free tier available |
Tech Stack
The tech stack used for this verification.
| Technology | Version |
|---|---|
| Ruby | 3.4.8 |
| Rails | 8.1.2 |
| PostgreSQL | 17 |
Application Structure
A minimal app with a scaffolded Post CRUD.
Routing
Rails.application.routes.draw do
resources :posts
get "up" => "rails/health#show", as: :rails_health_check
root "posts#index"
end| Path | Method | Description |
|---|---|---|
/ | GET | Post list (posts#index) |
/posts | GET | Post list |
/posts/new | GET | New post form |
/posts/:id | GET | Post detail |
/posts/:id/edit | GET | Edit form |
/up | GET | Health check (200 or 500) |
/up is a built-in Rails health check endpoint that returns 200 if the app boots successfully. It can be used directly as Railway’s health check.
Dockerfile
We use the multi-stage Dockerfile generated by Rails 8.1 as-is.
ARG RUBY_VERSION=3.4.8
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
WORKDIR /rails
# ... (base packages, jemalloc, environment variables)
FROM base AS build
# Install build dependencies, gems, precompile assets
COPY . .
RUN bundle install
RUN bundle exec bootsnap precompile -j 1 app/ lib/
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
FROM base
# Non-root user, copy artifacts
COPY --chown=rails:rails --from=build /rails /rails
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 3000
CMD ["./bin/rails", "server"]The key point is the separation of ENTRYPOINT and CMD. bin/docker-entrypoint automatically runs db:prepare before starting rails server.
#!/bin/bash -e
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
./bin/rails db:prepare
fi
exec "${@}"This mechanism becomes important when configuring Railway, as discussed later.
Deploying to Railway
1. Create railway.json
Add railway.json to the root of your repository.
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE"
},
"deploy": {
"healthcheckPath": "/up",
"healthcheckTimeout": 300,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 5
}
}| Setting | Value | Description |
|---|---|---|
builder | DOCKERFILE | Build using Dockerfile |
healthcheckPath | /up | Rails health check endpoint |
healthcheckTimeout | 300 | Health check timeout in seconds |
restartPolicyType | ON_FAILURE | Restart on failure |
restartPolicyMaxRetries | 5 | Maximum retry count |
2. Create a Project in the Dashboard
Follow these steps in the Railway Dashboard:
- Create a new project — Connect your GitHub repository
- Add PostgreSQL — From the project canvas, select “Add Service” → “PostgreSQL”
- Set environment variables — In the app service’s Variables tab, configure the following
| Variable | Value |
|---|---|
DATABASE_URL | ${{Postgres.DATABASE_URL}} |
RAILS_MASTER_KEY | Value from config/master.key |
For DATABASE_URL, use Railway’s variable reference syntax ${{Postgres.DATABASE_URL}}. This automatically injects the PostgreSQL service’s connection information.
3. Verify the Deployment
When you push to your GitHub repository, Railway automatically builds and deploys. Once deployment completes, you can access your app at a *.up.railway.app domain.
PR Preview Environments
This is the main topic. Railway has a built-in feature that automatically creates isolated preview environments for each PR.
Configuration
Simply enable Enable PR Environments in Project Settings → Environments. No GitHub Actions workflows needed.
How It Works
When a PR is created, Railway automatically performs the following:
| Step | Description |
|---|---|
| 1. Create environment | Create an isolated environment dedicated to the PR |
| 2. Provision DB | Provision a new PostgreSQL instance |
| 3. Build | Build the Dockerfile with the PR branch code |
| 4. DB migration | Run db:prepare (via docker-entrypoint) |
| 5. Issue URL | Assign a unique *.up.railway.app URL |
| 6. Notify GitHub | Post deployment results as a comment on the PR |
When the PR is merged or closed, the preview environment is automatically deleted.
PR Preview in Action
In PR #5, the Railway bot automatically posted the following comment:
Deployed to the rails-preview-demo-pr-5 environment in rails-preview-demo
- Service: rails-preview-demo — Status: Success
- Web URL:
https://rails-preview-demo-rails-preview-demo-pr-5.up.railway.app
Since a unique URL is issued for each PR, reviewers can verify changes simply by clicking the link. The database is also isolated, so you can safely create test data or perform destructive operations.
Bot PR Environments — A Note for AI Tool Users
When using AI tools like Claude Code or GitHub Copilot to create commits and PRs, Railway classifies them as Bot PRs. By default, preview environments are not created for Bot PRs.
To enable preview environments for AI-generated PRs, enable Bot PR Environments in Project Settings → Environments.
If you forget this, you’ll encounter a situation where preview environments don’t spin up only for PRs created by AI tools.
Gotcha — startCommand Conflicting with docker-entrypoint
Here’s an issue I encountered during testing.
Problem
Initially, I had set startCommand in railway.json.
{
"deploy": {
"startCommand": "bin/rails db:prepare && bin/rails server -b ::",
"healthcheckPath": "/up"
}
}With this configuration, production deployment succeeded but preview environment deployment failed.
Cause
Railway passes startCommand as shell arguments to the Dockerfile’s ENTRYPOINT. This means the actual command executed looks like:
/rails/bin/docker-entrypoint "bin/rails db:prepare && bin/rails server -b ::"
docker-entrypoint checks whether the last two arguments are ./bin/rails and server to determine whether to run db:prepare. When startCommand is passed, the arguments become a single string, so the condition doesn’t match and db:prepare is skipped.
Solution
Remove startCommand and let the Dockerfile’s ENTRYPOINT + CMD handle everything. This is the same railway.json shown earlier, with startCommand removed.
ENTRYPOINT(docker-entrypoint) detectsrails serverand runsdb:prepareCMD(./bin/rails server) starts Puma- Puma binds to
0.0.0.0by default and reads thePORTenvironment variable set by Railway
If the Dockerfile is properly configured, there’s no need to specify startCommand on the Railway side. The Dockerfile generated by Rails 8.1 works on Railway as-is.
Summary
With Railway, you can set up PR preview environments for your Rails app with minimal configuration.
- Only one config file needed:
railway.json - No GitHub Actions workflows required
- Isolated environments with their own DB spin up automatically for each PR
- Environments are automatically deleted on merge/close
- Don’t forget to enable Bot PR Environments when using AI tools
- Skip
startCommandand let the Dockerfile’s ENTRYPOINT + CMD handle it
Having PR preview environments changes your review workflow. No more pulling branches locally to test — just click the PR link and verify the changes.
That’s all from the Gemba — deploying Rails to Railway and setting up per-PR preview environments.