Docs / Programming & Development / Deploy Payload CMS

Deploy Payload CMS

By Admin · Mar 15, 2026 · Updated Apr 23, 2026 · 111 views · 3 min read

Payload CMS is a modern headless CMS built with TypeScript that provides a code-first approach to content modeling. Unlike GUI-based CMS platforms, Payload defines collections and fields in code, making it version-controllable and developer-friendly. This guide covers deploying Payload on a VPS.

Project Setup

# Create Payload project
npx create-payload-app@latest my-cms
cd my-cms

# Select template and database (MongoDB or PostgreSQL)
# PostgreSQL is recommended for new projects

Collection Configuration

// src/collections/Posts.ts
import type { CollectionConfig } from "payload";

export const Posts: CollectionConfig = {
  slug: "posts",
  admin: {
    useAsTitle: "title",
    defaultColumns: ["title", "status", "author", "createdAt"],
  },
  access: {
    read: () => true,
    create: ({ req: { user } }) => Boolean(user),
    update: ({ req: { user } }) => Boolean(user),
    delete: ({ req: { user } }) => user?.role === "admin",
  },
  fields: [
    { name: "title", type: "text", required: true },
    { name: "slug", type: "text", unique: true, admin: { position: "sidebar" } },
    {
      name: "content",
      type: "richText",
    },
    {
      name: "status",
      type: "select",
      defaultValue: "draft",
      options: [
        { label: "Draft", value: "draft" },
        { label: "Published", value: "published" },
      ],
    },
    {
      name: "author",
      type: "relationship",
      relationTo: "users",
    },
    {
      name: "featuredImage",
      type: "upload",
      relationTo: "media",
    },
    { name: "publishedAt", type: "date", admin: { position: "sidebar" } },
  ],
};

Payload Configuration

// src/payload.config.ts
import { buildConfig } from "payload";
import { postgresAdapter } from "@payloadcms/db-postgres";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import { s3Storage } from "@payloadcms/storage-s3";
import { Posts } from "./collections/Posts";
import { Media } from "./collections/Media";
import { Users } from "./collections/Users";

export default buildConfig({
  admin: {
    user: Users.slug,
  },
  collections: [Users, Posts, Media],
  editor: lexicalEditor(),
  db: postgresAdapter({
    pool: {
      connectionString: process.env.DATABASE_URI!,
    },
  }),
  plugins: [
    s3Storage({
      collections: { media: true },
      bucket: process.env.S3_BUCKET!,
      config: {
        credentials: {
          accessKeyId: process.env.S3_ACCESS_KEY!,
          secretAccessKey: process.env.S3_SECRET_KEY!,
        },
        region: process.env.S3_REGION,
      },
    }),
  ],
  secret: process.env.PAYLOAD_SECRET!,
  typescript: { outputFile: "src/payload-types.ts" },
});

Building and Deploying

# Build for production
npm run build

# Start production server
NODE_ENV=production npm start

# PM2 deployment
pm2 start dist/server.js --name payload-cms
pm2 save && pm2 startup

Systemd Service

[Unit]
Description=Payload CMS
After=network.target postgresql.service

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/payload-cms
ExecStart=/usr/bin/node dist/server.js
EnvironmentFile=/opt/payload-cms/.env
Environment=NODE_ENV=production
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Docker Deployment

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

API Usage

# REST API (auto-generated)
curl https://cms.example.com/api/posts?where[status][equals]=published

# GraphQL (if enabled)
curl -X POST https://cms.example.com/api/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ Posts { docs { title slug content } } }"}'

# TypeScript SDK
import { getPayloadClient } from "payload";
const payload = await getPayloadClient();
const posts = await payload.find({
  collection: "posts",
  where: { status: { equals: "published" } },
});

Summary

Payload CMS provides a code-first approach to headless content management that developers appreciate. Collections defined in TypeScript are version-controllable, type-safe, and testable. The auto-generated admin panel, REST and GraphQL APIs, and flexible access control system make it suitable for everything from blogs to complex enterprise content workflows. Self-hosting on a VPS provides full control over data, unlimited API calls, and the ability to extend with custom endpoints and hooks.

Was this article helpful?