Deploy Next.js + PostgreSQL + Prisma on Railway: Beginner's Guide


Tags: nextjs postgresql prisma railway full-stack

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:

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?

  1. prisma generate - Creates Prisma Client types from your schema
  2. prisma migrate deploy - Applies pending migrations to production database
  3. next 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:

  1. Click “New Project”
  2. Click “Empty Project”
  3. Click ”+ New” → “Database” → “Add PostgreSQL”
  4. Railway provisions the database in ~30 seconds

Step 3: Connect Your Repository to Railway

  1. In Railway dashboard, click ”+ New” → “GitHub Repo”
  2. Select your repository
  3. Railway detects Next.js and configures build settings automatically

Step 4: Link Database to Your Service

  1. In your Next.js service, go to “Variables” tab
  2. Click ”+ New Variable” → “Add Reference”
  3. Select your PostgreSQL service → DATABASE_URL
  4. 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:

  1. In Variables tab, find the DATABASE_URL reference
  2. Check “Build Time” checkbox (critical!)
  3. 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:

  1. Connect to your local DATABASE_URL (from .env)
  2. Create migration files in prisma/migrations/
  3. 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:

  1. Go to Railway dashboard → PostgreSQL service → Variables tab
  2. Copy the DATABASE_PUBLIC_URL value
  3. Paste it into your local .env file as DATABASE_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:

  1. npm install - Installs all dependencies
  2. prisma generate - Creates TypeScript types from schema
  3. prisma migrate deploy - Applies the migration files you just created
  4. next build - Builds the Next.js application
  5. 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 3000 or similar success message

Common Issues:

SymptomCauseSolution
”DATABASE_URL is not defined”Build-time variable not enabledCheck “Build Time” checkbox in Variables tab
”Cannot reach database”Database connection issueVerify DATABASE_URL is correctly set in Variables tab
”Module not found: @prisma/client”Prisma generate didn’t runVerify build command includes prisma generate
Page shows blank/white screenBuild failed silentlyCheck 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)