Site icon Ampersand Tutorials

Deploying Payload CMS to Production (2026)

Quick answer: Production Payload is mostly a Next.js deployment plus three decisions you make once: run real migrations instead of dev schema-push, point the database at a managed Postgres (or MongoDB/SQLite) with a non-dev connection string, and put media and backups on infrastructure that survives a server rebuild. The app itself can live anywhere Next.js 15.2.9–15.4.x or 16.2.6+ runs — Vercel, a Node host, Docker, or a VPS — but the database and the object storage are the pieces that determine how painful a restore will be.

Part 4 of our Payload CMS series. Start with what Payload CMS is, then install Payload 3 and build your first collection, then authentication and role-based access control before you ship.

1. What “production” actually changes

In development, Payload pushes schema changes to the database automatically when you change the config. That is convenient while you are iterating, and it is the source of the most common production surprise: a field you added locally is simply absent on the deployed server because no one ran a migration.

Production changes three things at once:

The application code itself is still a normal Next.js app. If you can deploy Next.js, you can deploy Payload. The trick is getting the things around it right.

2. Database: pick one and treat it seriously

Payload 3 supports Postgres, MongoDB and SQLite through dedicated adapters. For a real site, Postgres is the safest default: relational schemas, mature managed providers, and SQL tooling you can use when Payload is not the only thing reading the database.

DatabaseGood fitWatch out for
PostgresMost production sites, relational content, Drizzle interoperabilityMigrations must be run; dev push and migrations should not mix on the same database
MongoDBDeeply nested documents, teams already on MongoSchema drift is less visible; migrations still exist but are less central
SQLiteLocal prototypes, tiny internal toolsNot a shared-production choice; backups are file copies, not managed

A managed Postgres provider gives you a DATABASE_URL in the form postgres://user:password@host:port/db. Put that in an environment variable, not in the config file. The config should read process.env.DATABASE_URL and fail loudly when it is missing, not silently fall back to a local file.

// payload.config.ts — production-ready db block
import { postgresAdapter } from '@payloadcms/db-postgres'

export default buildConfig({
  // ...
  db: postgresAdapter({
    pool: { connectionString: process.env.DATABASE_URL },
  }),
})

Do not mix development schema-push and migrations against the same database. If you prototype by pushing in development, create a fresh database for the migration workflow before you treat it as production. The adapter can be configured in push mode in development and migration mode in production, but the same physical database should not see both styles.

3. Migrations: the step teams skip and then pay for

Development pushes schema automatically. Production needs migrations:

npx payload migrate:create add_published_at
npx payload migrate

migrate:create writes a migration file with up and down functions — it does not run anything. Review the generated file before you commit it. migrate applies pending migrations. In a deployment pipeline, migrations run as part of the deploy, before the new app starts serving traffic that expects the new schema.

Two habits prevent the classic failure:

If you also use Drizzle to query Payload tables directly, run npx payload generate:db-schema and import from the generated file so your hand-written queries stay in sync with what Payload actually created. That generation step is the bridge between “Payload knows the schema” and “my custom SQL query still compiles.”

4. Where media lives, and why local uploads are a trap

By default Payload can store uploads locally. That is fine for a prototype on one machine. It is a bad default for anything that might run on more than one server, get redeployed, or need to survive a restore.

A place to hang related singletons off a parent global comes later; when you need content that exists only once in production, read globals, hooks and reused collections alongside this deployment guide.

The practical production move is object storage. Configure the upload adapter you need — S3-compatible storage is the most common choice — and let media live outside the app server’s filesystem. That way redeploying the app does not wipe uploaded files, and a restore does not need a forgotten media directory to come back with it.

The sharp image processor also belongs in the config when you resize or transform uploads. Install sharp and pass it into the config so Payload can generate variants. Without it, some image operations fall back or fail depending on your adapter and field configuration.

import sharp from 'sharp'

export default buildConfig({
  // ...
  sharp,
})

Treat media the same way you treat the database: something the app references by configuration, not something the app owns in its deploy directory.

5. Secrets: least surprise, least surface

Payload needs a secret for signing sessions and tokens. It also needs whatever your database and storage adapters require. The rule is boring and worth stating anyway:

# .env (never committed)
PAYLOAD_SECRET=a-long-random-unguessable-string
DATABASE_URL=postgres://user:password@host:5432/db

In config, read them with a clear failure mode:

secret: process.env.PAYLOAD_SECRET || '',

An empty PAYLOAD_SECRET is the kind of thing that lets the admin panel start, then fails in confusing ways once sessions or tokens are involved. Better to crash on missing config than to ship with one.

6. Deployment targets and what they change

Payload is a Next.js app with a server component. That means the deployment target is mostly “where does Next.js run,” with a few Payload-specific notes.

TargetWhat changes
Vercel / serverless Next.js hostsAdmin panel and API routes run as serverless functions; make sure the provider supports the runtime Payload needs and that long-running tasks (seeds, large migrations) fit the platform’s limits
Node server / VPS / DockerMore control, longer-running process, easier local media and background jobs; you manage the database and storage wiring
Edge-focused hostingPayload’s admin and REST/GraphQL layers are server-side; an edge-only runtime is not a drop-in fit for the parts that need Node

The (payload) route group from the blank template mounts the admin panel and the REST/GraphQL routes. Those files are boilerplate — you keep them and move your own routes into another group. On deployment, the question is whether the hosting model runs those server routes the way Payload expects.

If you deploy to a platform that runs builds and then serves from a static output, make sure the server-side routes are still present and executable. A pure static export is not the same as a server deployment. This is the point where “it works on localhost” diverges from “it works after deploy.”

7. A sensible deploy sequence

A repeatable deploy for a Payload site:

If your host supports build-time and runtime steps, put migrations in the runtime step, not the build step, unless the host explicitly runs them at the right point. The goal is: schema ready before the new code expects it.

8. Health checks and observability worth adding

A production CMS should answer a few questions without SSH:

A lightweight health endpoint is enough to start. Something that initializes Payload, reads a known document or global, and returns a small JSON answer tells you more than “the server responded.” Keep it out of the public content routes if you can, or at least make it cheap and read-only.

For an editorial site, also watch the things that actually hurt: failed uploads, migration failures in deploy logs, and auth reset email delivery. The admin panel working means little if the password reset flow is broken or media is silently failing against misconfigured storage.

9. The errors you will actually hit after deploy

SymptomCause and fix
Admin panel 404 in production(payload) route group missing or routes not wired; check the route group and the withPayload wrapper
New field missing in productionDevelopment pushed the schema but no migration ran — run migrate:create and migrate and commit the migration
Database connection errorDATABASE_URL missing or wrong in the environment; check the managed database credentials and network access
Uploads fail or images do not transformStorage adapter not configured, credentials wrong, or sharp missing from the config
Sessions or logins behave oddlyPAYLOAD_SECRET missing or changed between deploys — a changed secret invalidates existing sessions
Custom SQL query is broken after a deploySchema changed and the Drizzle/schema generation was not re-run — regenerate and import the updated schema
Reset password emails do not arriveAuth reset URLs point at an unreachable host in production — set them explicitly for the environment
App starts, then errors under loadA Local API call is running with overrideAccess: true on behalf of a visitor — pass overrideAccess: false and a user where appropriate

10. Key takeaways and challenge

Challenge: take the editorial site from the previous article and write the production wiring for it. Define the environment variables you would set, the migration you would run after adding editorialNotes and the review status, the storage setup you would use for uploaded images, and a one-page deploy checklist that includes a restore test. Then write down — before you touch any code — which step fails first if the PAYLOAD_SECRET is changed between deploys, and what that breaks for existing users.

Shipping a Payload site and want a second pair of eyes on the migration and storage plan? Ampersand Academy offers one-to-one mentoring on TypeScript and Next.js backends — part of Mahadhi, a development and digital marketing company.

What do I need to change to move Payload from development to production?

Three things: run real migrations instead of dev schema push, point the database at a managed connection string via environment variable, and put media on object storage outside the app server. The app itself is a normal Next.js deployment once those are in place.

Do I have to use migrations in production?

Yes. Development can push schema automatically, but production should apply migrations so the change history is explicit and the deployed schema matches what the code expects. migrate:create writes a migration file and migrate applies it.

Which database should I use for a production Payload site?

Postgres is the safest default for most sites because managed providers are mature and the schema is relational. MongoDB fits deeply nested documents, and SQLite is for prototypes, not shared production.

Why not keep uploads in the app public folder?

Because redeploying the app can wipe or reshuffle that directory, and a restore needs the media back too. Object storage keeps media independent of the app deploy, which makes both deploys and restores less fragile.

What happens if PAYLOAD_SECRET changes between deploys?

Existing sessions and tokens can become invalid because they were signed with the old secret. Set the secret per environment, keep it stable for a given environment, and test how a change affects active sessions before you rotate it in production.

Can I use Payload on a serverless host?

Usually yes, because the admin panel and API routes run as server-side Next.js routes. The main caveats are platform limits on long-running tasks and making sure the hosting model actually runs the server routes Payload expects, not just a static output.

How do I know a production deploy is healthy?

Check that the admin panel logs in, a known collection returns documents through the Local API, media uploads and transforms work against the configured storage, and that migrations are current. A backup you have restored once beats a backup you have only made.

Exit mobile version