Deploy Next.js + PostgreSQL + Prisma on Railway: Beginner's Guide
Deploy a production-ready blog with Next.js, PostgreSQL, and Prisma on Railway in 20 minutes. This guide builds a secure, read-only application that’s safe to deploy publicly.
What you’ll build: A blog application that displays posts from PostgreSQL, with type-safe database queries using Prisma and Server-Side Rendering with Next.js
Prerequisites: Node.js 18+, Git, Railway account
Prerequisites
Technical Requirements:
- Node.js 18.17 or later (required for Next.js 14 native fetch support)
- npm 9+ or pnpm 8+ for package management
- Git installed and configured (
git --version) - Code editor with TypeScript support (VS Code recommended)
Railway Setup:
- Railway account (sign up here)
Knowledge Prerequisites:
- Basic JavaScript/Node.js familiarity
- Basic command line usage (running commands, navigating directories)
- Have used React or Next.js before (or willing to follow along carefully)
Project Setup
Create a new Next.js project with TypeScript:
npx create-next-app@latest my-railway-app
When prompted, select the following options:
✔ Would you like to use TypeScript? › Yes
✔ Would you like to use ESLint? › Yes
✔ Would you like to use Tailwind CSS? › Yes
✔ Would you like your code inside a `src/` directory? › Yes
✔ Would you like to use App Router? › Yes
✔ Would you like to use Turbopack for `next dev`? › No
✔ Would you like to customize the import alias (@/* by default)? › No
Then navigate to the project:
cd my-railway-app
Install Prisma and PostgreSQL client:
npm install @prisma/client
npm install -D prisma
Initialize Prisma in your project:
npx prisma init
This creates two files:
prisma/schema.prisma- Your database schema definition.env- Environment variables file
Verify .env is in .gitignore:
cat .gitignore | grep ".env"
# Should output: .env
If not present, add it:
echo ".env" >> .gitignore
Configure build command in package.json:
Open package.json and find the "build" line in the "scripts" section. Replace it with the following:
"build": "prisma generate && prisma migrate deploy && next build"
Before:
"build": "next build"
After:
"build": "prisma generate && prisma migrate deploy && next build"
Why this order?
prisma generate- Creates Prisma Client types from your schemaprisma migrate deploy- Applies pending migrations to production databasenext build- Builds optimized Next.js production bundle
Prisma Schema & Database
Open prisma/schema.prisma and replace the entire file contents with the following schema:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}
Note: The npx prisma init command created a starter schema with SQLite as the provider. We’re replacing it to use PostgreSQL and add our User/Post models.
Key schema features:
@id @default(autoincrement())- Simple auto-incrementing integer IDs (1, 2, 3…)@unique- Ensures email addresses are unique across users@relation- Links posts to their authors@default(now())- Automatically sets creation timestamp
Create a new directory and file for the Prisma client. Create src/lib/prisma.ts:
mkdir -p src/lib
touch src/lib/prisma.ts
Then add the following code to src/lib/prisma.ts:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;
This creates a simple Prisma client that you can import and use throughout your application.
Application Code
This guide builds a read-only blog application that displays posts from your database. Read-only APIs are safe to expose publicly and don’t require authentication, making this a secure, production-ready deployment.
For applications that need user-generated content (create, update, delete operations), you’ll need to implement authentication - see the “Next Steps for Production” section at the end.
Create API route:
Create the directory and file for the API route:
mkdir -p src/app/api/posts
touch src/app/api/posts/route.ts
Add the following code to src/app/api/posts/route.ts:
import { NextResponse } from "next/server";
import prisma from "../../../lib/prisma";
export async function GET() {
try {
const posts = await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { name: true, email: true },
},
},
orderBy: { createdAt: "desc" },
take: 10,
});
return NextResponse.json({ posts });
} catch (error) {
console.error("Database error:", error);
return NextResponse.json(
{ error: "Failed to fetch posts" },
{ status: 500 }
);
}
}
Update homepage:
Open src/app/page.tsx and replace the entire file contents with the following:
import prisma from "../lib/prisma";
export default async function Home() {
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<main className="max-w-4xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-8">Latest Posts</h1>
<div className="space-y-6">
{posts.map((post) => (
<article key={post.id} className="border p-6 rounded-lg">
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
<p className="text-gray-600 mb-4">{post.content}</p>
<p className="text-sm text-gray-500">
By {post.author.name ?? "Anonymous"} •{" "}
{new Date(post.createdAt).toLocaleDateString()}
</p>
</article>
))}
</div>
</main>
);
}
Why Server Components? Fetching data directly in Server Components reduces JavaScript bundle size and improves performance. No client-side data fetching libraries needed.
Deployment
Now that all your application code is ready, let’s deploy to Railway.
Step 1: Commit Your Code
Commit all your application code:
git add .
git commit -m "Add complete blog application with Prisma schema and API routes"
git push origin main
Step 2: Create Railway Project and Database
Log in to Railway dashboard:
- Click “New Project”
- Click “Empty Project”
- Click ”+ New” → “Database” → “Add PostgreSQL”
- Railway provisions the database in ~30 seconds
Step 3: Connect Your Repository to Railway
- In Railway dashboard, click ”+ New” → “GitHub Repo”
- Select your repository
- Railway detects Next.js and configures build settings automatically
Step 4: Link Database to Your Service
- In your Next.js service, go to “Variables” tab
- Click ”+ New Variable” → “Add Reference”
- Select your PostgreSQL service →
DATABASE_URL - Railway creates a reference like:
${{Postgres.DATABASE_URL}}
Note: The reference name (e.g., “Postgres”) matches your PostgreSQL service name. If you renamed your database service, the reference will reflect that name (e.g., ${{MyDatabase.DATABASE_URL}}).
Step 5: Enable Build-Time Variables
Prisma needs DATABASE_URL during build for prisma generate. Railway’s variable reference system makes the database URL available at both build time and runtime.
To configure:
- In Variables tab, find the
DATABASE_URLreference - Check “Build Time” checkbox (critical!)
- This makes the variable available during both build and runtime
Once you save this, Railway will automatically start deploying your application.
Step 6: Create and Apply Database Migrations
Now that Railway is deploying, you need to create migration files locally and push them so Railway can apply them to the database.
In your local terminal, create the initial migration:
npx prisma migrate dev --name init
This will:
- Connect to your local DATABASE_URL (from
.env) - Create migration files in
prisma/migrations/ - Prompt you that the database doesn’t match the schema
You’ll see an error because your .env still has the default localhost DATABASE_URL. This is expected. Update your .env to use Railway’s database temporarily:
- Go to Railway dashboard → PostgreSQL service → Variables tab
- Copy the
DATABASE_PUBLIC_URLvalue - Paste it into your local
.envfile asDATABASE_URL
Now run the migration command again:
npx prisma migrate dev --name init
This creates the migration files in prisma/migrations/.
Step 7: Deploy Migrations to Railway
Commit and push the migration files:
git add .
git commit -m "Add initial database migration"
git push origin main
Railway will redeploy and this time the build will succeed because:
- npm install - Installs all dependencies
- prisma generate - Creates TypeScript types from schema
- prisma migrate deploy - Applies the migration files you just created
- next build - Builds the Next.js application
- Deploy - Starts your application
Watch deployment logs in Railway dashboard. Deployment typically completes in 2-3 minutes.
Step 8: Verify Your Deployed Application
Once deployment completes (2-3 minutes), test your application:
1. Visit Your Homepage
In Railway dashboard, click your Next.js service and then click the generated domain link (e.g., your-app-production.up.railway.app).
✅ You should see:
- Page Title: “Latest Posts”
- Empty state - No posts displayed yet (we’ll add seed data in the next section)
- Proper styling with borders, padding, and spacing
2. Verify API Endpoint
Visit your-app.railway.app/api/posts to confirm the API is working.
Expected: Empty posts array (we haven’t seeded data yet):
{
"posts": []
}
3. Check Deployment Logs
In Railway dashboard → Service → Logs, verify:
- ✅ No “DATABASE_URL” errors or Prisma connection failures
- ✅
Server listening on port 3000or similar success message
Common Issues:
| Symptom | Cause | Solution |
|---|---|---|
| ”DATABASE_URL is not defined” | Build-time variable not enabled | Check “Build Time” checkbox in Variables tab |
| ”Cannot reach database” | Database connection issue | Verify DATABASE_URL is correctly set in Variables tab |
| ”Module not found: @prisma/client” | Prisma generate didn’t run | Verify build command includes prisma generate |
| Page shows blank/white screen | Build failed silently | Check Railway deployment logs for errors |
Populate Production Data
Your application is deployed successfully, but the database is empty. Let’s populate it with initial blog posts using Railway CLI.
Step 1: Create Seed Script
Create prisma/seed.ts:
mkdir -p prisma
touch prisma/seed.ts
Add the following code to prisma/seed.ts:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
// Create a test user
const user = await prisma.user.upsert({
where: { email: "test@example.com" },
update: {},
create: {
email: "test@example.com",
name: "Test User",
},
});
console.log("✅ Created user:", user.email);
// Create test posts
const posts = await Promise.all([
prisma.post.create({
data: {
title: "Getting Started with Next.js",
content: "Learn how to build modern web applications with Next.js 14.",
published: true,
authorId: user.id,
},
}),
prisma.post.create({
data: {
title: "Prisma Best Practices",
content: "Tips for using Prisma effectively in production.",
published: true,
authorId: user.id,
},
}),
]);
console.log(`✅ Created ${posts.length} posts`);
}
main()
.catch((e) => {
console.error("❌ Seed error:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Step 2: Configure Seed Command
Add to your package.json:
{
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"scripts": {
"dev": "next dev",
"build": "prisma generate && prisma migrate deploy && next build",
"start": "next start",
"lint": "next lint",
"seed": "tsx prisma/seed.ts"
},
"devDependencies": {
"tsx": "^4.7.0"
}
}
Step 3: Install tsx
npm install -D tsx
Step 4: Commit and Push
git add .
git commit -m "Add database seed script"
git push origin main
Wait for Railway to redeploy (this adds the tsx dependency and seed script to your production environment).
Step 5: Run Seed via Railway CLI
Install Railway CLI if you haven’t already:
npm install -g @railway/cli
Login to Railway:
railway login
Link to your project (run this from your project directory):
railway link
Select your project from the list.
Run the seed script against your production database:
railway run npm run seed
Expected output:
✅ Created user: test@example.com
✅ Created 2 posts
Step 6: Verify Data Was Added
Refresh your deployed application homepage. You should now see:
- Two blog posts displayed:
- “Getting Started with Next.js” by Test User
- “Prisma Best Practices” by Test User
- Formatted dates showing when posts were created
Visit /api/posts endpoint - you should see a JSON response with 2 posts.
Optional: Verify in Railway Dashboard → PostgreSQL service → Data tab - you should see 1 user and 2 posts.
What you’ve accomplished:
- ✅ Production-ready blog application deployed to Railway
- ✅ PostgreSQL database connected and populated
- ✅ Type-safe database operations with Prisma
- ✅ Secure read-only API endpoints at
/api/posts - ✅ Server-side rendering with React Server Components
- ✅ Automatic SSL certificate and CDN delivery
Next steps for adding user-generated content:
This guide intentionally focuses on read-only operations to avoid security vulnerabilities. If you want to add write operations (create, update, delete posts), you’ll need to implement authentication first.
Recommended authentication solutions:
- NextAuth.js - Open-source, flexible, supports multiple providers (free)
- Clerk - Drop-in authentication with UI components (free tier available)
- Auth0 - Enterprise-grade authentication (free tier available)
What authentication enables:
- User registration and login
- Protected API routes (POST, PUT, DELETE)
- User-specific content (my posts, drafts, etc.)
- Role-based access control (admin, editor, viewer)
Additional production improvements:
- Set up separate development and production databases
- Add rate limiting to prevent API abuse
- Implement database connection pooling for high traffic
- Add monitoring and error tracking (Sentry, LogRocket)