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.