Why self-host Payload CMS on a VPS
Payload CMS takes a radically code-first approach: your content schema is defined in TypeScript in configuration files, versioned with Git, and since its version 3 Payload installs directly into a Next.js application via the App Router. This means a VPS lets you host the CMS and the front-end site in a single Node process, sharing the build and the runtime. You get end-to-end typing, schema migrations managed in code, and no dependency on a graphical interface to model your content. Self-hosting is the natural option here: Payload is designed to be deployed like any Next.js app, and a VPS gives you control of the database (MongoDB or PostgreSQL), the uploads and the environment variables, without an intermediary platform.
Concrete benefits
- Content schema defined in TypeScript and versioned with Git: full code review and history
- CMS and Next.js front end in a single runtime: one build, one process to deploy
- End-to-end typing between the Payload config, the API and the front end, without manual generation
- Choice of database: MongoDB or PostgreSQL via the official adapters
- Schema migrations driven by code (
payload migrate), reproducible across environments - Local API: direct data access without an HTTP call from Next.js server code
Hardware and software prerequisites
Since Payload relies on Next.js, the build is demanding: plan for a VPS with 2 vCPU and 4 GB of RAM to build and run the application comfortably, especially if the Next.js front end is substantial. Install Node.js 20 LTS, Docker and Docker Compose. On the database side, provision MongoDB 7 or PostgreSQL 16 depending on the chosen adapter (@payloadcms/db-mongodb or @payloadcms/db-postgres). Point your domain at the VPS IP. Count 15 GB of disk for the node_modules, the .next build, the uploads and the database backups.
Step-by-step deployment
Choose and configure the database adapter
In payload.config.ts, declare the adapter: mongooseAdapter for MongoDB or postgresAdapter for PostgreSQL, reading the connection URL from DATABASE_URI. This choice is structural: PostgreSQL requires migrations, MongoDB is more flexible on the schema.
Set the secret and build the Next.js app
Set PAYLOAD_SECRET (the token encryption key) in the environment. Run npm run build: since Payload 3 lives inside Next.js, this command compiles both the Payload admin and your front end. Watch the RAM during the build.
Containerize with Docker Compose
Create a node:20-alpine Dockerfile that builds the app and runs npm start, then a docker-compose.yml linking the app service to a mongo or postgres service. Mount a volume for the uploads and pass DATABASE_URI and PAYLOAD_SECRET as variables. Run docker compose up -d.
Run the migrations (PostgreSQL)
If you use the PostgreSQL adapter, apply the schema with npm run payload migrate after the containers start. With MongoDB, the schema is applied automatically, no manual migration is needed.
Configure Nginx as a reverse proxy
Point a vhost to http://127.0.0.1:3000 (Next.js default port). Increase client_max_body_size for media uploads and add the X-Forwarded-Proto headers so that Payload generates correct HTTPS URLs.
Secure with SSL and set the server URL
Run certbot --nginx -d mydomain.com, then set serverURL: 'https://mydomain.com' in payload.config.ts. This URL conditions the media links, the reset emails and the correct operation of the admin panel behind the proxy.
Payload CMS vs Strapi: which headless CMS to self-host?
| Criterion | Payload CMS | Strapi |
|---|---|---|
| Configuration approach | Code-first in TypeScript, versioned with Git | GUI-first via the Content-Type Builder |
| Front-end integration | Native in Next.js (a single runtime) | Decoupled, front end and CMS separate |
| Supported databases | MongoDB and PostgreSQL | PostgreSQL, MySQL, SQLite |
| Typing | Native end-to-end TypeScript | Generated types, looser integration |
| Server data access | Local API without an HTTP call | REST/GraphQL API via HTTP |
| Content modeling | In the code, by developers | In the interface, accessible to non-developers |
| RAM needed for the build | High (Next.js build) | High (React admin build) |
| Ideal for | Dev teams, typed Next.js projects | Mixed teams, visual modeling |
Leverage Payload's Local API in your Next.js server components: instead of calling your own API via fetch, import getPayload and query the database directly (payload.find({ collection: 'posts' })). You remove an HTTP round-trip and gain in latency as well as in security. For media, plug in the @payloadcms/storage-s3 plugin to store uploads off the VPS, and automate a daily dump of your database via a cron to guarantee off-server backups.