Payload is a code-first, TypeScript-native headless CMS. Since version 3, it runs natively inside Next.js, which changes how you deploy it: you are shipping a Next.js app that happens to expose an admin panel and a REST/GraphQL API. With the PostgreSQL adapter, it slots neatly into a standard container deploy.
Choose the Postgres adapter
Payload supports MongoDB and SQL databases through swappable adapters. For Postgres:
npm install @payloadcms/db-postgres// payload.config.ts
import { postgresAdapter } from '@payloadcms/db-postgres';
import { buildConfig } from 'payload';
export default buildConfig({
secret: process.env.PAYLOAD_SECRET,
db: postgresAdapter({
pool: { connectionString: process.env.DATABASE_URL }
}),
collections: [
{
slug: 'posts',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'richText' }
]
}
]
});The PAYLOAD_SECRET signs JWTs and encrypts sensitive fields. Generate a strong one and never reuse it across environments:
openssl rand -base64 32Migrations: the part people skip
Payload's Postgres adapter generates real SQL migrations. In development it can push schema changes automatically, but in production you want deterministic, reviewable migrations.
# Generate a migration from your current config
npx payload migrate:create
# Apply pending migrations
npx payload migrateThe golden rule: run payload migrate as a deploy step before the new app serves traffic. Set push: false on the adapter in production so it never silently alters tables:
postgresAdapter({
pool: { connectionString: process.env.DATABASE_URL },
push: process.env.NODE_ENV !== 'production'
});Media storage
Payload uploads default to the local filesystem. In a containerized world that disk is ephemeral — every restart wipes uploaded files. Use a cloud storage adapter:
npm install @payloadcms/storage-s3import { s3Storage } from '@payloadcms/storage-s3';
// inside buildConfig plugins:
s3Storage({
collections: { media: true },
bucket: process.env.S3_BUCKET,
config: {
region: process.env.S3_REGION,
credentials: {
accessKeyId: process.env.S3_KEY,
secretAccessKey: process.env.S3_SECRET
}
}
})Any S3-compatible store works — AWS S3, Cloudflare R2, MinIO. This is non-negotiable for production; skipping it is the most common reason "my images disappeared."
Build and run
Because Payload 3 is a Next.js app, the production commands are the Next.js ones:
npm run build # next build
npm start # next start, defaults to PORT 3000FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=build /app ./
ENV NODE_ENV=production
EXPOSE 3000
CMD ["npm", "start"]Deploying on PandaStack
- 1Provision a managed PostgreSQL (14.x or 16.x). PandaStack injects
DATABASE_URLautomatically. - 2Connect the Git repo as a container app. The Dockerfile is auto-detected; if you skip the Dockerfile, the Node buildpack handles
next build/next start. - 3Set
PAYLOAD_SECRET, the S3 credentials, andNODE_ENV=productionas environment variables. - 4Run
npx payload migrateonce before exposing traffic. - 5Add your domain; SSL is automatic.
The admin panel lives at /admin, the REST API at /api, and GraphQL at /api/graphql. Create your first admin user by visiting /admin after the first successful deploy.
Production checklist
- [ ]
push: falsein the Postgres adapter for production. - [ ] Migrations run as an explicit deploy step.
- [ ] Cloud storage adapter configured (no local uploads).
- [ ]
PAYLOAD_SECRETset and unique per environment. - [ ]
serverURL/CORS configured if your frontend is on a different origin. - [ ] Connection pool sized for your instance (start small, 10–20).
Verifying
# API should respond
curl -s https://cms.example.com/api/posts | head
# Admin panel loads
curl -s -o /dev/null -w '%{http_code}' https://cms.example.com/adminA reachable /api and a 200 on /admin mean your database connection and build are healthy.
References
- [Payload PostgreSQL adapter docs](https://payloadcms.com/docs/database/postgres)
- [Payload migrations guide](https://payloadcms.com/docs/database/migrations)
- [Payload S3 storage plugin](https://payloadcms.com/docs/upload/storage-adapters)
- [Payload production deployment](https://payloadcms.com/docs/production/deployment)
---
Payload is a clean fit for PandaStack: container deploy plus a managed PostgreSQL with DATABASE_URL injected automatically. Try it free at [dashboard.pandastack.io](https://dashboard.pandastack.io).