Skip to main content

deployment

This skill should be used when the user asks to "deploy Payload to production", "deploy Payload to Vercel", "dockerize a Payload app", "build without a database connection", "add rate limiting", "prevent API abuse", "optimize Payload performance", or "configure serverless database connections". Covers the production build, Vercel and Docker/Node self-host targets, building without a live DB, locking down REST/GraphQL, and query performance.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
November 1, 2026 at 02:46
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
deployment
description
This skill should be used when the user asks to "deploy Payload to production", "deploy Payload to Vercel", "dockerize a Payload app", "build without a database connection", "add rate limiting", "prevent API abuse", "optimize Payload performance", or "configure serverless database connections". Covers the production build, Vercel and Docker/Node self-host targets, building without a live DB, locking down REST/GraphQL, and query performance.
# PayloadCMS — Production & Deployment Ship a PayloadCMS v3 app to production. Payload runs anywhere Next.js runs — it is a Next.js app, so `next build` is the build. This skill covers the production build, deploy targets, building without a DB connection, hardening the API against abuse, and query performance. ## 1. Production build Payload's build is Next.js's build. The scaffolder wires `package.json` so `pnpm build` runs `next build`: ```bash # Standard production build pnpm build # → next build pnpm start # → next start (serves the .next output) ``` Required environment in every production environment: ```bash # .env (production) PAYLOAD_SECRET=<long-random-string> # signs JWTs/tokens — keep stable, never commit DATABASE_URI=postgres://user:pass@host:5432/db NEXT_PUBLIC_SERVER_URL=https://cms.example.com NODE_ENV=production ``` `PAYLOAD_SECRET` must be long and impossible to guess — rotating it logs every user out and invalidates reset tokens. Set `serverURL` in the config (absolute, protocol + domain, no path) so emails, previews, and CORS resolve correctly: ```ts // src/payload.config.ts export default buildConfig({ serverURL: process.env.NEXT_PUBLIC_SERVER_URL, // … }) ``` Before deploying: turn off schema auto-sync and run migrations instead (`push: false` on Postgres/SQLite — see `cli-recipes` for `migrate` in CI/CD), and confirm cookies are secure when behind SSL. ## 2. Deploy targets | Target | DB | File storage | Watch out for | |--------|----|--------------|---------------| | **Vercel / serverless** | External, pooled (PgBouncer, Neon, Vercel Postgres) | **Cloud adapter required** (S3/R2/Blob) — local FS is ephemeral | Function timeouts; connection exhaustion; cold starts | | **Docker / Node self-host** | Any reachable DB | Cloud adapter or persistent volume | `output: 'standalone'`; non-root user; `sharp` in runner | | **Payload Cloud** | Managed | Managed | Push-to-deploy; least config | ### Vercel & serverless Serverless functions are short-lived and run concurrently, so two rules are non-negotiable: - **File storage must be a cloud adapter.** The default writes uploads to disk, which vanishes between invocations. Use `@payloadcms/storage-s3`, `storage-vercel-blob`, etc. — see the `adapters` skill. - **Pool the database.** Each function instance opens its own connection; without pooling you exhaust `max_connections` fast. Front Postgres with a pooler (PgBouncer in transaction mode, Neon's pooled endpoint, Supabase pooler) and point `DATABASE_URI` at the pooled port. MongoDB Atlas pools via the driver but still cap `maxPoolSize` low per instance. Mind function execution limits — long imports, heavy `depth`, or big migrations can exceed the platform timeout. Run those out-of-band (a job/queue or a one-off Node task), not in a request. ### Docker / Node self-host Set standalone output so the image ships only the runtime files: ```js // next.config.mjs const nextConfig = { output: 'standalone' } export default nextConfig ``` A representative multi-stage Dockerfile (mirrors the official template): ```dockerfile # Dockerfile FROM node:22-alpine AS base FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json pnpm-lock.yaml* ./ RUN corepack enable pnpm && pnpm i --frozen-lockfile FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NODE_ENV=production RUN corepack enable pnpm && pnpm build # → next build (output: standalone) 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 COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV HOSTNAME="0.0.0.0" PORT=3000 CMD ["node", "server.js"] ``` Pass `PAYLOAD_SECRET`, `DATABASE_URI`, and `NEXT_PUBLIC_SERVER_URL` at runtime (compose `environment:`, `--env-file`, or your orchestrator's secrets). On Alpine, `libc6-compat` covers native deps; if image optimization fails in standalone mode, install `sharp` in the runner stage. With a persistent volume you may keep local file storage, but a cloud storage adapter is still the safer default. ### Payload Cloud Payload Cloud is the first-party managed host: connect a repo, push to deploy, and the DB plus file storage are provisioned for you. Lowest-config path when you don't want to manage infrastructure. ## 3. Building without a DB connection `next build` runs static generation, which can hit your database. In CI/CD — or any build environment with no DB reachable — compile without generating static pages using the Next.js build-mode flag (there is no Payload-specific env var for this). The official page is [Building without a DB connection](https://payloadcms.com/docs/v3/production/building-without-a-db-connection.md): ```bash # Compile only — no static generation, no DB connection needed pnpm next build --experimental-build-mode compile ``` Caveat: in `compile` mode, `NEXT_PUBLIC_*` vars are **not** inlined and read as `undefined` on the client. Resolve it with a second build step: ```bash pnpm next build --experimental-build-mode generate # DB reachable: generate static pages + inline env pnpm next build --experimental-build-mode generate-env # no DB reachable: inline NEXT_PUBLIC_* env only ``` Alternatively, mark routes that would otherwise statically render with `export const dynamic = 'force-dynamic'` — this avoids the build-time DB hit but disables static optimization, so pages render slower. ## 4. Preventing API abuse Payload v3 ships several config-level knobs. **Note:** the old top-level `rateLimit` config from v2 was removed in v3 — do request rate limiting in **Next.js middleware** or at a reverse proxy/CDN (Cloudflare, nginx `limit_req`), or with a package like `rate-limiter-flexible` keyed on the client IP. Don't add a `rateLimit` block to `buildConfig` — it is no longer a valid option. | Knob | Where | Effect | |------|-------|--------| | `maxDepth` | Config | Caps relationship traversal app-wide (default `10`) — stops runaway circular populate | | `defaultDepth` | Config | Depth used when a request omits `depth` | | `graphQL.disable: true` | Config | Removes the GraphQL endpoint entirely | | `graphQL.maxComplexity` | Config | Rejects over-complex GraphQL queries (relationship/upload fields cost 10) | | `maxLoginAttempts` / `lockTime` | Auth collection | Locks an account after N failed logins | | `access` | Collection/global/field | The real lock — gates every REST/GraphQL operation | | `disable` | Collection endpoints | Removes auto-generated REST/GraphQL operations you don't expose | - **Lock down with access control first.** Auto-generated REST and GraphQL endpoints obey each collection's `access` functions. An endpoint with no access restriction is open — test every `read`/`create`/`update`/`delete` function before going live (see the `access-control` skill). - **Disable GraphQL in production if unused.** Set `graphQL.disable: true` to drop the endpoint, or `graphQL.maxComplexity` to bound query cost. The GraphQL playground is not served in production builds. - **Keep `maxDepth` low.** It is the guardrail against malicious deep-populate queries that time out the server. - **Harden auth and uploads.** Set `maxLoginAttempts`/`lockTime` on auth collections; restrict `create`/`update`/`read` on upload collections and consider antivirus scanning in a hook. - **Upgrade to 3.90 promptly (security release).** It changes defaults you may rely on in production: API keys shown once (`useAPIKey: { reveal: true }` restores revealing), multipart uploads capped at 50 MiB per request and 20 MiB per file with 413 on excess (`upload.requestSizeLimit` and `upload.limits.fileSize`), strict SVG/XML validation, private Azure containers, a closed `payload-jobs` collection (3.89), and form-submission read access limited to the admin collection. After upgrading run `pnpm payload generate:types`, and on SQL databases `pnpm payload migrate:create` + `pnpm payload migrate` (adds `resetPasswordRequestedAt` and `_objectKey`). Details live in the `authentication`, `adapters`, `jobs-queue` and `official-plugins` skills. ## 5. Performance - **Index queried fields.** Add `index: true` to any field used in `where` filters or sorts to avoid full scans. Compound queries benefit from indexes on each leg. - **Discipline `depth`.** Every level of `depth` is another round of joins/lookups. Request the lowest depth a view needs; combine with `select` to fetch only required fields. - **Use `select` and `populate`/`defaultPopulate`.** `select` trims returned fields; `defaultPopulate` on a relationship collection controls which of its fields get pulled when populated elsewhere — both shrink payloads and DB load. - **Paginate.** Keep `limit` modest; pass `pagination: false` only for deliberate full exports. Skip the count query with `disableCount` when you don't render a total. - **Avoid N+1 in hooks.** Don't loop a `payload.find` per document inside a hook — batch with a single `where: { id: { in: [...] } }`. Thread `req` so nested ops reuse the transaction (see `adapters`). For hot paths, `payload.db.*` methods bypass hooks/validation: `await payload.db.updateOne({ collection, id, data, returning: false })`. - **Reuse the Payload instance.** Call `const payload = await getPayload({ config })` — it caches and reuses one instance; never instantiate per request. - **Cache at the framework layer.** Use Next.js ISR / `revalidate` / `revalidateTag` for read-heavy frontend pages instead of hitting Payload on every request — see `nextjs-integration`. <example> Production-hardened config excerpt — low depth, capped GraphQL, absolute URL: ```ts // src/payload.config.ts import { buildConfig } from 'payload' export default buildConfig({ serverURL: process.env.NEXT_PUBLIC_SERVER_URL, secret: process.env.PAYLOAD_SECRET || '', maxDepth: 5, // guardrail against deep-populate abuse defaultDepth: 1, // shallow by default; callers opt into more graphQL: { disable: process.env.NODE_ENV === 'production' && !process.env.ENABLE_GRAPHQL, maxComplexity: 1000, // reject over-complex queries when enabled }, cors: [process.env.NEXT_PUBLIC_SERVER_URL || ''], csrf: [process.env.NEXT_PUBLIC_SERVER_URL || ''], // db / collections / editor / storage omitted — see `adapters` }) ``` (Rate limiting is intentionally absent here — do it in Next.js middleware or a proxy, not in `buildConfig`.) </example> <example> CI build with no DB reachable, then generate static pages at container start: ```bash # CI: compile only — no DATABASE_URI needed pnpm next build --experimental-build-mode compile # Runtime entrypoint (DB now reachable): finish static generation pnpm next build --experimental-build-mode generate node .next/standalone/server.js ``` </example> ## What this skill does NOT cover - **Scaffolding a project, env basics, `PAYLOAD_SECRET` generation** — see the `setup` skill. - **Database / storage / email adapter config, connection pooling specifics, transactions** — see the `adapters` skill. - **ISR, `revalidate`, `revalidateTag`, caching at the Next.js layer** — see the `nextjs-integration` skill. - **`migrate:create` / `migrate` in CI/CD, `push: false` workflow** — see the `cli-recipes` skill. - **`depth`, `select`, `where`, and query construction in detail** — see the `queries` skill.
View on GitHub