"I deployed to Cloudflare Workers and got knocked out by the 30-second limit. Switched to Vercel, then couldn't keep persistent Postgres connections alive." I've been hearing this from indie developers building server-side pieces with Antigravity lately, and I've hit the same wall myself — usually when wiring up an image-generation proxy or a Stripe webhook retry queue.
Railway is the practical escape hatch I keep coming back to in those moments. It isn't fully edge like Cloudflare Workers, and it isn't serverless like Vercel Functions. It's a container-based PaaS that behaves more like a "lightweight VPS you don't have to maintain," which turns out to be a very natural fit for solo projects. In this guide, I'll walk through what it actually takes to get a typical Next.js + Postgres + worker setup onto Railway, using Antigravity's terminal and AI agent to keep the moving parts in check.
When Railway makes more sense than Cloudflare or Vercel
The starting point isn't a generic platform comparison — it's "where did your project actually get stuck?" In my experience, Railway becomes the right answer when one or more of these apply:
- Your code includes work that takes longer than 30 seconds (PDF generation, AI inference, video transcoding)
- You want a managed Postgres or Redis with persistent connections
- You need a background worker (cron jobs, queue consumers) running as a long-lived service
- You rely on a writable local filesystem for intermediate files
- You want to run an existing Dockerfile as-is without rewriting it for serverless
For purely static sites, lightweight APIs, or workloads that need extreme edge scale, Cloudflare Workers and Vercel are still better. Think of Railway as "the second deployment target you keep in your back pocket" for the moments when you need something a little thicker.
For background, my Vercel deployment workflow guide and Cloudflare Workers deployment notes cover the other two corners of the triangle. I'd recommend reading them alongside this piece before you commit to a stack.
Preparing your project from inside Antigravity
I'll use a fairly standard setup as the example: Next.js 15 + a Hono server + Postgres. Inside Antigravity, the prompt I usually hand to the agent is short:
Make this project deployable to Railway. Please:
- Add a multi-stage alpine Dockerfile
- Add railway.json with restartPolicyType=ON_FAILURE
- Verify the package.json start script honors PORT
- Add a .dockerignore
When the agent comes back with a diff, the two things you should actually read by hand are the Dockerfile and the start script. Railway injects a PORT environment variable, so a server that hardcodes a port will crash on boot.
# Dockerfile
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
# Don't hardcode a port; Railway injects PORT at runtime
EXPOSE 3000
CMD ["node", "server.js"]What to expect: with output: "standalone" enabled in your next.config, the final image lands around 180–220MB and cold-starts in 2–4 seconds. Without standalone mode, the image often balloons past 1GB, so check that flag first.
The minimum viable railway.json looks like this:
{
"$schema": "https://railway.app/railway.schema.json",
"build": { "builder": "DOCKERFILE", "dockerfilePath": "Dockerfile" },
"deploy": {
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10,
"healthcheckPath": "/api/healthz",
"healthcheckTimeout": 30
}
}The healthcheck is what I'd actually consider the most useful field. If the new container crashes on boot, Railway rolls back to the previous revision automatically, which has saved me more than once after late-night refactors.
Use Antigravity's review pass to flush out the obvious failures
This is where pairing Railway with an AI IDE earns its keep. After the scaffold is in place, I send the agent another prompt:
Before we deploy this to Railway, please review the codebase strictly for:
1) Any code that doesn't honor the PORT env variable
2) Persistent writes to the local filesystem
3) ORM settings that might explode connections (Prisma prepared statements, pool size)
4) Bare cron / setInterval usage
5) Secrets accidentally exposed via NEXT_PUBLIC_
In practice, this review almost always surfaces one or two real fixes. Prisma projects are the worst offender for me — without an explicit connection_limit, Railway's Postgres tends to saturate within minutes of going live and you end up watching your service throw 502s.
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
datasources: {
db: {
// Append ?connection_limit=5 to Railway's DATABASE_URL
url: process.env.DATABASE_URL,
},
},
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;What to expect: for indie-scale traffic, connection_limit=5 is plenty. While you don't yet have a clear feel for concurrency, picking a fixed number well below your Postgres plan's ceiling is the boring move that prevents 2 a.m. fire drills.
Wire up environment variables with Reference variables
One of Railway's nicer ergonomics is Reference variables. Once you add the Postgres plugin, your web service can pull the connection URL from ${{Postgres.DATABASE_URL}} instead of manually copying it.
DATABASE_URL=${{Postgres.DATABASE_URL}}
REDIS_URL=${{Redis.REDIS_URL}}
NEXT_PUBLIC_SITE_URL=https://${{RAILWAY_STATIC_URL}}
NEXTAUTH_URL=https://${{RAILWAY_STATIC_URL}}
NEXTAUTH_SECRET=YOUR_NEXTAUTH_SECRET
What to expect: when Railway rotates plugin credentials or URLs, you don't have to touch your service config — Reference variables resolve at deploy time. I ask Antigravity to "rewrite my .env.example assuming Railway Reference variables," and it produces a clean file like the one above in a single step.
For custom domains, add the domain in the Railway dashboard and point a CNAME at XXXX.up.railway.app. If you're fronting Railway with Cloudflare, I'd suggest connecting in DNS-only mode first to verify, then enabling the proxy. This makes troubleshooting TLS or header rewrites much easier.
Run web, worker, and cron from one repository
Railway lets you spin up multiple services from the same repository, which is a perfect match for a typical indie SaaS layout:
- web: the Next.js + Hono API
- worker: a queue consumer (BullMQ or similar)
- cron: a once-a-day batch job
You can keep a single Dockerfile and override the start command per service in Railway's UI.
{
"scripts": {
"build": "next build",
"start": "node server.js",
"start:worker": "node dist/worker.js",
"start:cron": "node dist/cron.js"
}
}What to expect: pointing the web service to npm run start and the worker to npm run start:worker is enough — three processes, one repo, one Dockerfile. Compared to Cloudflare Workers Cron or Vercel Cron, long-running jobs and always-on workers are noticeably more straightforward on Railway.
If you're shaping a queue topology, my Antigravity multi-tool orchestration notes are a useful companion for deciding how aggressively to split your workers.
The honest constraints to weigh before you commit
A few practical limits worth knowing before you push real traffic:
- The free tier is execution-time based (around 500 hours of credit per month at the time of writing). Two always-on workers will burn through it fast
- Regions span US / EU / Asia-Southeast / Asia-Northeast, but mixing regions across services adds inter-service latency. Keep them aligned by default
PORTmust be honored — any hardcoded port crashes immediately- No built-in CDN. Pair Railway with Cloudflare R2 or Vercel Edge Network for static assets if you care about global delivery
- Logs are stdout-based. Investing in structured logging makes
railway logsfiltering far less painful
I capture all of these in my agents.md so future projects inherit the same pre-deploy checklist instead of relearning the lessons each time.
Closing thought — keep Railway as your "second deployment target"
Even when Cloudflare Workers and Vercel cover 80% of your projects, the moments where you need long-running tasks, persistent connections, or always-on workers will arrive. Setting up a single Railway service today, before you actually need it, means you have a working escape route the next time you hit those walls.
For a concrete next step: scan your current project for any task that doesn't comfortably finish within 30 seconds. If even one comes to mind, copy the Dockerfile and railway.json from this article and ask Antigravity to "wire up an initial Railway deploy." You should have a staging environment running in about 15 minutes.