7 KiB
| tags | topic | sources | created | ||||||
|---|---|---|---|---|---|---|---|---|---|
|
payloadcms |
|
2026-05-15 |
PayloadCMS — Production & Performance
Overview
Payload runs fully inside Next.js, so the Next.js build pipeline applies everywhere. Deploy anywhere Next.js runs: Vercel, Netlify, Docker/VPS, SST, AWS, Cloudflare Workers.
Required infra checklist before going live:
- Database (Postgres or MongoDB) in the same region as the server
- Persistent file storage (S3 / GCS / Azure Blob) if using uploads
- Email provider
- CDN (optional but recommended)
Setup
Build
npm run build # runs next build
npm run start # runs next start (production mode)
Environment variables (minimum)
PAYLOAD_SECRET=<long-random-string> # must be hard to brute-force
DATABASE_URL=<connection-string>
PAYLOAD_CONFIG_PATH=dist/payload.config.js # if needed
Key Patterns
Docker (multi-stage, standalone)
Set output: 'standalone' in next.config.js, then use this Dockerfile pattern:
FROM node:24-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
else yarn --frozen-lockfile; fi
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
elif [ -f package-lock.json ]; then npm run build; \
else yarn run build; fi
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next && chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD HOSTNAME="0.0.0.0" node server.js
Building without a DB connection (Docker CI pattern)
Next.js SSG attempts to call Payload Local API at build time → fails if no DB available.
Option 1 — experimental build mode (recommended):
# Step 1: compile only (no DB needed, no static generation)
pnpm next build --experimental-build-mode compile
# Step 2: generate static pages (when DB is available, e.g. at deploy time)
pnpm next build --experimental-build-mode generate
# If DB still not available at generate time, inline NEXT_PUBLIC_ vars:
pnpm next build --experimental-build-mode generate-env
Option 2 — opt out of SSG entirely (slower, simpler):
Add to every route segment that uses Payload Local API:
export const dynamic = 'force-dynamic'
Note: disables static optimization, every page renders dynamically on request.
Abuse prevention
| Threat | Config |
|---|---|
| Brute-force login | maxLoginAttempts + lockTime on auth collections |
| Deep relationship bombs | maxDepth on Payload config (default: 10, set as low as possible) |
| GraphQL complexity attacks | graphQL.maxComplexity (each field = 1, relationships/uploads = 10) |
| Unused GraphQL surface | graphQL.disable: true |
| CSRF | Built-in — configure allowed origins in csrf array |
| CORS | cors array in Payload config |
| Malicious uploads | Restrict create/update access on upload collections; scan via ClamAV hook |
Cloud storage adapters for uploads
Avoid ephemeral filesystems (Heroku, DigitalOcean Apps). Official adapters:
@payloadcms/storage-s3— AWS S3@payloadcms/storage-gcs— Google Cloud Storage@payloadcms/storage-azure— Azure Blob@payloadcms/storage-vercel-blob— Vercel@payloadcms/storage-uploadthing
CosmosDB compatibility
import { mongooseAdapter, compatibilityOptions } from '@payloadcms/db-mongodb'
db: mongooseAdapter({
url: process.env.DATABASE_URL,
...compatibilityOptions.cosmosdb, // disables multi-doc transactions, uses find instead of subqueries
indexSortableFields: true,
})
Config / Code Examples
Secure cookie settings
Enable in each auth collection config when TLS is in place:
auth: {
cookies: { secure: true, sameSite: 'strict' }
}
Direct DB calls for performance
Bypasses hooks, validation, access control — use only when safe:
// Update without fetching the document back (Postgres + MongoDB)
await payload.db.updateOne({
collection: 'posts',
id: post.id,
data: { title: 'New Title' },
returning: false, // skip fetching updated doc, saves a round-trip
})
Block references (reduce config bloat)
// Define once at root
const config = buildConfig({
blocks: [{ slug: 'TextBlock', fields: [{ name: 'text', type: 'text' }] }],
collections: [
{ slug: 'posts', fields: [{ name: 'content', type: 'blocks', blockReferences: ['TextBlock'], blocks: [] }] },
{ slug: 'pages', fields: [{ name: 'content', type: 'blocks', blockReferences: ['TextBlock'], blocks: [] }] },
],
})
Reuse cached Payload instance
import { getPayload } from 'payload'
import config from '@payload-config'
const payload = await getPayload({ config }) // returns cached instance, safe to call repeatedly
Dev speed (Next.js 15)
{ "scripts": { "dev": "next dev --turbo" } }
Skip bundling server packages during dev (enabled by default in create-payload-app ≥ v3.28.0):
export default withPayload(nextConfig, { devBundleServerPackages: false })
Selective imports from @payloadcms/ui (frontend bundle)
// Bad — bundles entire UI library
import { Button } from '@payloadcms/ui'
// Good — tree-shaken, use in non-admin code
import { Button } from '@payloadcms/ui/elements/Button'
Admin Panel custom components can still use import { Button } from '@payloadcms/ui' — they are not bundled into the frontend.
Gotchas
- Heroku / DigitalOcean Apps use ephemeral filesystems — uploads vanish on restart; use a cloud storage adapter
next buildwith SSG + Local API requires DB — use--experimental-build-mode compilein Docker CI to avoid this- CosmosDB does not support multi-document transactions — spread
compatibilityOptions.cosmosdbpreset maxDepthdefaults to 10 — set it as low as your UI allows; circular relationships can crash the serverpayload.secretin production must be cryptographically strong — used to sign JWTs and cookies- Direct DB calls skip hooks/validation — never use for user-facing write paths
returning: falseonly available onpayload.db.*methods, not the Local APINEXT_PUBLIC_*vars not inlined incompilemode — rungenerate-envif you need them without a DB
Related
- wiki/payloadcms/database — migrations, indexes, transactions, Postgres vs MongoDB
- wiki/payloadcms/getting-started — initial project setup and configuration overview