Railway 2026: Why Indie Hackers Are Switching from Vercel
Railway's transparent pricing and Docker support attract developers leaving Vercel. We break down the migration, real errors you'll hit, and production patterns.
TL;DR
Railway is gaining traction among indie hackers because of transparent, predictable pricing ($5/month minimum vs. Vercel's usage surprises), native Docker support, and PostgreSQL included. We'll show you real migration errors, production patterns, and when Railway makes sense for your stack.
---
Why the Exodus from Vercel?
The indie hacker community isn't abandoning Vercel—but Railway is capturing mindshare. Three patterns emerge:
1. Bill Shock Prevention Vercel's per-function execution pricing (as of 2025) can surprise at scale. Railway's model is simpler: $5/month minimum, then $0.000927/CPU-second. You know what you'll pay. [Verify current pricing in official docs](https://railway.app/pricing).
2. Database Included Railway bundles PostgreSQL (5GB free tier, verify current limits). Vercel requires external databases (Supabase, Neon, PlanetScale). That's one fewer vendor.
3. Docker-First Philosophy Railway deploys any Dockerfile. Vercel optimizes for Node/Python serverless functions. If your stack is Go, Rust, or custom—Railway feels native.
---
Real Errors You'll Hit During Migration
Error #1: Environment Variable Scope Confusion
``` Error: Cannot find module 'dotenv' at Function.Module._load (internal/modules/commonjs/loader.js:1159:3) at Module.require (internal/modules/commonjs/loader.js:1185:41) ```
Why it happens: Railway injects env vars directly—no .env file needed. If your app calls require('dotenv').config() in production, it'll fail when dotenv isn't installed.
Fix: ```javascript if (process.env.NODE_ENV !== 'production') { require('dotenv').config(); }
const dbUrl = process.env.DATABASE_URL; ```
Railway v0.8.1+ (current stable) supports variable grouping. Test locally with Railway's CLI: ```bash railway run npm start ```
Error #2: Port Binding on Railway
``` Error: listen EADDRINUSE: address already in use :::3000 ```
Why it happens: Railway assigns a random port via $PORT env var. Hard-coding 3000 fails.
Production pattern: ```javascript const PORT = process.env.PORT || 3000; const HOST = process.env.HOST || '0.0.0.0';
app.listen(PORT, HOST, () => {
console.log(Server running on port ${PORT});
});
```
Error #3: Build Command Not Found
``` error: No such file or directory: /app/build.sh Command exited with status 128 ```
Why it happens: Railway uses Procfile or railway.json to define build/start commands. If you skip this and Railway guesses wrong, you'll see cryptic build failures.
Production pattern - railway.json:
```json
{
"build": {
"builder": "dockerfile"
},
"deploy": {
"startCommand": "node dist/server.js"
}
}
```
Or use Procfile (verify [Railway docs on Procfiles](https://docs.railway.app/deploy/builds#procfile)):
```
web: npm run start
worker: npm run worker
```
---
Migration Checklist: Vercel → Railway
1. Prepare Your Dockerfile
Railway prefers explicit Dockerfiles for production workloads. Here's a battle-tested Node pattern:
```dockerfile
Build stage
FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=productionRuntime stage
FROM node:20-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY . .EXPOSE 3000 CMD ["node", "server.js"] ```
2. Map Environment Variables
Export from Vercel, import to Railway:
```bash
Vercel side
vercel env pull .env.localCopy sensitive vars to Railway dashboard
Railway → Your Project → Variables
```3. Database Migration
If using Vercel + Supabase, pg_dump to Railway's PostgreSQL:
```bash
Get Railway DB URL from dashboard
railway run bashInside Railway environment
pg_restore --clean --if-exists -d $DATABASE_URL < dump.sql ```4. Deploy & Monitor
Railway auto-deploys on git push (if connected to GitHub). Watch logs:
```bash railway logs --follow ```
---
When Railway Wins vs. Vercel
Choose Railway if:
Stick with Vercel if:
See [Docker deployments explained](/?guide=docker-basics) and [database strategy for indie apps](/?guide=db-selection).
---
Production Patterns on Railway
Health Checks
Railway expects your app to exit on failure. Add a health endpoint:
```javascript app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: Date.now() }); }); ```
Configure in railway.json:
```json
{
"healthcheck": {
"path": "/health",
"interval": 30000
}
}
```
Secrets Management
Never commit secrets. Use Railway's Variables tab:
```bash
Good: Railway injects at runtime
const apiKey = process.env.STRIPE_SECRET_KEY;Never do this
const apiKey = 'sk_live_...'; ```Background Jobs
Railway supports multiple processes per deployment. Create a worker service:
```yaml
railway.json
{ "services": [ { "name": "api", "startCommand": "node api.js" }, { "name": "worker", "startCommand": "node worker.js" } ] } ```---
Pricing Reality Check (2026)
Verify current limits: [Railway pricing page](https://railway.app/pricing)
For a typical indie SaaS (2 CPU, 1GB RAM, 10GB DB): expect $20-40/month.
---
What am I missing?
Railway's ecosystem moves fast. Tell us in comments:
We'll update this based on your real experience. Comment below.