Deploy Payload CMS with DeployHQ
Prerequisites:
- Node.js 18 or later
- MongoDB or PostgreSQL
- SSH access to your server
- A DeployHQ account
What is Payload CMS?
Payload is a headless CMS built with TypeScript and Node.js. It's code-first: you define collections, fields, and access control in TypeScript config files — no GUI-based schema building. Payload auto-generates an admin panel, REST API, and GraphQL API from your configuration.
Payload 3.0 is built on Next.js, combining the CMS backend and frontend into a single application.
Step 1: Create a Payload Project
npx create-payload-app@latest my-cms
cd my-cms
npm run dev
The admin panel runs at http://localhost:3000/admin.
Step 2: Project Structure
my-cms/
├── src/
│ ├── app/ # Next.js app directory
│ │ ├── (frontend)/ # Public-facing pages
│ │ └── (payload)/ # Admin panel routes
│ ├── collections/ # Content type definitions
│ │ ├── Posts.ts
│ │ ├── Pages.ts
│ │ ├── Media.ts
│ │ └── Users.ts
│ ├── globals/ # Singleton content types
│ │ ├── Header.ts
│ │ └── Footer.ts
│ ├── blocks/ # Reusable content blocks
│ └── payload.config.ts # Main Payload configuration
├── public/ # Static assets
├── media/ # Uploaded files (if local storage)
├── next.config.mjs
├── tsconfig.json
├── package.json
└── .env # Environment variables
Step 3: Configuration
// src/payload.config.ts
import { buildConfig } from 'payload'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
// or: import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { s3Storage } from '@payloadcms/storage-s3'
import { Posts } from './collections/Posts'
import { Pages } from './collections/Pages'
import { Media } from './collections/Media'
import { Users } from './collections/Users'
export default buildConfig({
admin: {
user: Users.slug,
},
collections: [Posts, Pages, Media, Users],
editor: lexicalEditor(),
db: mongooseAdapter({
url: process.env.DATABASE_URI!,
}),
plugins: [
s3Storage({
collections: { media: true },
bucket: process.env.S3_BUCKET!,
config: {
region: process.env.S3_REGION!,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
},
}),
],
})
Collection Definition
// src/collections/Posts.ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
},
access: {
read: () => true,
create: ({ req: { user } }) => !!user,
update: ({ req: { user } }) => !!user,
delete: ({ req: { user } }) => !!user,
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', unique: true },
{ name: 'content', type: 'richText' },
{ name: 'publishedDate', type: 'date' },
{ name: 'status', type: 'select', options: ['draft', 'published'] },
{
name: 'author',
type: 'relationship',
relationTo: 'users',
},
{
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
},
],
}
Step 4: Core Features
- Auto-generated Admin UI — CRUD interface for all collections
- REST API —
GET /api/posts,POST /api/posts, etc. - GraphQL API — Full GraphQL endpoint with auto-generated schema
- Authentication — Built-in user authentication with role-based access
- File Uploads — Local storage, S3, or other cloud storage
- Hooks — Before/after change, validate, and access control hooks
- Localization — Built-in i18n for content fields
- Versions & Drafts — Content versioning and draft/publish workflow
Step 5: Build for Production
npm run build
Payload 3.0 builds as a Next.js application. Output goes to .next/.
Step 6: Deploy with DeployHQ
Build Command
cd %path% && npm ci && npm run build
PM2 Configuration
Create ecosystem.config.cjs:
module.exports = {
apps: [{
name: 'payload-cms',
script: 'node_modules/.bin/next',
args: 'start',
instances: 'max',
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
max_memory_restart: '1G',
}]
}
Post-Deployment SSH Command
cd /var/www/payload-cms && npm ci --omit=dev && pm2 reload ecosystem.config.cjs --env production
Environment Variables
Set on the server in .env (never commit to Git):
DATABASE_URI=mongodb://localhost:27017/payload-cms
PAYLOAD_SECRET=a-long-random-string
NEXT_PUBLIC_SERVER_URL=https://cms.example.com
# S3 storage (if using)
S3_BUCKET=my-uploads
S3_REGION=eu-west-1
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=...
Nginx Reverse Proxy
server {
listen 443 ssl http2;
server_name cms.example.com;
client_max_body_size 100M; # For file uploads
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Media Storage
For production, use S3 or similar cloud storage for uploads. If using local storage, configure media/ as a shared path in DeployHQ's zero-downtime deployment settings.
Step 7: Troubleshooting
Build Fails with Memory Error
Payload + Next.js builds can be memory-intensive:
bash
NODE_OPTIONS="--max-old-space-size=4096" npm run build
Database Connection Refused
Ensure MongoDB/PostgreSQL is running and the DATABASE_URI is correct. For MongoDB, check authentication:
bash
mongosh --eval "db.runCommand({ connectionStatus: 1 })"
Admin Panel Returns 404
Ensure the Next.js server is running and Nginx is proxying correctly. Check PM2 status:
bash
pm2 status payload-cms
pm2 logs payload-cms
Migrations (PostgreSQL)
Payload with PostgreSQL uses Drizzle ORM for migrations:
bash
npx payload migrate
Run this post-deployment if schema changes exist.
Conclusion
Payload CMS combines the flexibility of a headless CMS with the power of Next.js — all defined in TypeScript. DeployHQ handles the build and deployment pipeline, including PM2 process management for zero-downtime updates.
Sign up for DeployHQ — free for one project.
Questions? Contact support@deployhq.com or on Twitter/X.