**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

```bash
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

```typescript
// 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

```typescript
// 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

```bash
npm run build
```

Payload 3.0 builds as a Next.js application. Output goes to `.next/`.

## Step 6: Deploy with DeployHQ

### Build Command

```bash
cd %path% && npm ci && npm run build
```

### PM2 Configuration

Create `ecosystem.config.cjs`:

```javascript
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

```bash
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):

```bash
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

```nginx
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](https://www.deployhq.com/signup) — free for one project.

Questions? Contact [support@deployhq.com](mailto:support@deployhq.com) or on [Twitter/X](https://x.com/deployhq).
