---
tags: [payloadcms, tech-patterns]
topic: payloadcms
sources: [production__deployment.md, production__preventing-abuse.md, production__building-without-a-db-connection.md, performance__overview.md]
created: 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

```bash
npm run build   # runs next build
npm run start   # runs next start (production mode)
```

### Environment variables (minimum)

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

```dockerfile
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):**

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

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

```ts
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:

```ts
auth: {
  cookies: { secure: true, sameSite: 'strict' }
}
```

### Direct DB calls for performance

Bypasses hooks, validation, access control — use only when safe:

```ts
// 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)

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

```ts
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)

```json
{ "scripts": { "dev": "next dev --turbo" } }
```

Skip bundling server packages during dev (enabled by default in `create-payload-app` ≥ v3.28.0):

```ts
export default withPayload(nextConfig, { devBundleServerPackages: false })
```

### Selective imports from `@payloadcms/ui` (frontend bundle)

```ts
// 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 build` with SSG + Local API requires DB** — use `--experimental-build-mode compile` in Docker CI to avoid this
- **CosmosDB does not support multi-document transactions** — spread `compatibilityOptions.cosmosdb` preset
- **`maxDepth` defaults to 10** — set it as low as your UI allows; circular relationships can crash the server
- **`payload.secret` in 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: false` only available on `payload.db.*` methods**, not the Local API
- **`NEXT_PUBLIC_*` vars not inlined in `compile` mode** — run `generate-env` if you need them without a DB

## Related

- [[wiki/payloadcms/database|Database]] — migrations, indexes, transactions, Postgres vs MongoDB
- [[wiki/payloadcms/getting-started|Getting Started]] — initial project setup and configuration overview
