|Docs

Self-host Payload CMS with Postgres

deploymentfrontendpayloadcms

Payload CMS is a TypeScript-native headless CMS that runs inside a Next.js application. Starting with version 3.0, Payload embeds directly in your Next.js app, sharing the same server, routes, and build process.

This guide covers deploying Payload CMS 3.x on Railway with a Postgres database and media storage.

What you will set up

  • A Payload CMS instance running inside Next.js
  • A Postgres database for content storage
  • Media uploads via a Railway Storage Bucket
  • A public domain for the admin panel and API

Create a Payload project

npx create-payload-app@latest my-payload-app

Select the Postgres database adapter and your preferred template during setup. Push the project to a GitHub repository.

Deploy to Railway

1. Create the project and database

  1. Create a new project on Railway.
  2. Add a PostgreSQL database: click + New, then Database, then PostgreSQL.
  3. Deploy the app: click + New, then GitHub Repo, and select your repository.

2. Configure environment variables

Add the following variables to the Payload service:

DATABASE_URI=${{Postgres.DATABASE_URL}}
PAYLOAD_SECRET=<generate a random string>
NEXT_PUBLIC_SERVER_URL=https://your-domain.railway.app

Generate the secret with:

openssl rand -base64 32

Set NEXT_PUBLIC_SERVER_URL after generating a domain (next step). This variable tells Payload its public URL for generating links in the admin panel.

3. Configure standalone output

Set output: "standalone" in next.config.js:

import { withPayload } from '@payloadcms/next/withPayload';

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
};

export default withPayload(nextConfig);

4. Generate a domain

Navigate to Networking in the service settings and generate a domain. Update the NEXT_PUBLIC_SERVER_URL variable to match the generated domain.

Open https://your-domain.railway.app/admin to create your first admin user.

Media storage

By default, Payload stores media on the local filesystem, which is ephemeral on Railway. Configure an S3-compatible storage provider for persistent media.

  1. Create a Storage Bucket in your Railway project.
  2. Install the storage adapter:
npm install @payloadcms/storage-s3
  1. Add the adapter to your Payload config:
// payload.config.ts
import { s3Storage } from '@payloadcms/storage-s3';

export default buildConfig({
  plugins: [
    s3Storage({
      collections: { media: true },
      bucket: process.env.BUCKET_NAME!,
      config: {
        credentials: {
          accessKeyId: process.env.BUCKET_ACCESS_KEY_ID!,
          secretAccessKey: process.env.BUCKET_SECRET_ACCESS_KEY!,
        },
        endpoint: process.env.BUCKET_ENDPOINT,
        region: 'auto',
      },
    }),
  ],
  // ... rest of config
});
  1. Add the bucket credentials to your service variables.

Connecting a frontend

Payload exposes a REST API at /api and a GraphQL API at /api/graphql. Since Payload runs inside Next.js, you can also use Server Components to query content directly:

// app/page.tsx
import { getPayload } from 'payload';
import config from '@payload-config';

export default async function Home() {
  const payload = await getPayload({ config });
  const posts = await payload.find({ collection: 'posts' });

  return (
    <ul>
      {posts.docs.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

For a separate frontend (e.g., a standalone React app), call the REST API:

const response = await fetch('https://your-payload.railway.app/api/posts');
const { docs } = await response.json();

Build memory

Payload with Next.js can be memory-intensive during builds, especially with many collections and plugins. If the build fails with an out-of-memory error, increase the Node.js heap size by adding this variable:

NODE_OPTIONS=--max-old-space-size=4096

Next steps