[{"data":1,"prerenderedAt":207},["ShallowReactive",2],{"seo-verification":3,"blog-deploying-a-payload-cms-application-on-a-vps-ma-en":6,"promo-bar-likely":206},{"google":4,"bing":5},"EycwPY2XMyTkVzas3n1ygeNJFGAH513qrMjfDljzsMQ","",{"key":7,"data":8},"blog-deploying-a-payload-cms-application-on-a-vps-ma-en",{"id":9,"slug":10,"slugs":11,"title":15,"excerpt":16,"readTime":17,"views":18,"isPinned":19,"noindex":19,"publishedAt":20,"updatedAt":21,"category":22,"categories":28,"featuredImage":30,"bgImage":31,"posterImage":32,"relatedSolution":30,"intro":33,"sections":34,"ctaTitle":145,"ctaBody":146,"ctaButton":147,"ctaUrl":148,"relatedPosts":149},55,"deploying-a-payload-cms-application-on-a-vps",{"fr":12,"en":10,"ar":13,"es":14},"deployer-payload-cms-vps","نشر-تطبيق-payload-cms-على-خادم-vps","desplegar-payload-cms-en-un-vps","Deploy Payload CMS on a VPS: complete guide","Complete guide to self-hosting Payload CMS 3 on a VPS: Docker Compose, auth, hooks, migrations, and real-world error troubleshooting.",10,2,false,"2026-04-26T00:00:00+00:00","2026-11-08T01:13:14+00:00",{"id":23,"name":24,"slug":25,"color":26,"icon":27},4,"Development","developpement","bg-warning\u002F10 text-warning","dev",[29],{"id":23,"name":24,"slug":25,"color":26,"icon":27},null,"\u002Fblog\u002Fcovers\u002Fbg.svg","\u002Fblog\u002Fcovers\u002Fdeployer-payload-cms-vps-poster.svg","Payload CMS 3 installs directly inside a Next.js application, making it one of the most code-native headless CMS options available. This guide covers the full deployment on a VPS: Docker Compose with healthchecks, user and API key management, collection hooks, migration workflow, and troubleshooting the most common production errors.",[35,39,50,53,75,78,81,84,87,90,142],{"type":36,"title":37,"body":38},"h2","Why self-host Payload CMS on a VPS","Payload CMS takes a radically code-first approach: your content schema is defined in TypeScript configuration files, versioned with Git, and since version 3 Payload installs directly inside a Next.js application via the App Router. This means a VPS lets you host both the CMS and the front-end in a single Node process, sharing the same build and runtime. You get end-to-end typing, schema migrations managed in code, and no dependency on a GUI to model your content. Self-hosting is the natural choice here: Payload is designed to be deployed like any Next.js app, and a VPS gives you full control over the database (MongoDB or PostgreSQL), uploads, and environment variables, without any intermediary platform.",{"type":40,"title":41,"items":42},"ul","Concrete benefits",[43,44,45,46,47,48,49],"Content schema defined in TypeScript and versioned with Git: full code review and history","CMS and Next.js front in a single runtime: one build, one process to deploy","End-to-end typing between Payload config, API and front, no manual generation","Database choice: MongoDB, PostgreSQL or SQLite via official adapters","Code-driven schema migrations (`payload migrate`), reproducible across environments","Local API: direct data access without HTTP calls from Next.js server code","No licensing cost: Payload CMS 3 is open-source (MIT) — you only pay for the VPS",{"type":36,"title":51,"body":52},"Hardware and software requirements","Since Payload runs on Next.js, the build is demanding: plan for a VPS with 2 vCPU and 4 GB RAM to build and run the application comfortably, especially if the Next.js front is large. Install Node.js 22 or 24 (Payload 3.93 requires `^22.23.4 || >=24.22.1`; Node 20 is no longer accepted), Docker and Docker Compose. On the database side, provision MongoDB 7 or PostgreSQL 16 depending on your chosen adapter (`@payloadcms\u002Fdb-mongodb` or `@payloadcms\u002Fdb-postgres`). Point your domain to the VPS IP. Budget 15 GB of disk space for `node_modules`, the `.next` build, uploads and database backups.",{"type":54,"title":55,"steps":56},"steps","Step-by-step deployment",[57,60,63,66,69,72],{"title":58,"body":59},"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 is a structural choice: PostgreSQL requires migrations, MongoDB is more flexible on schema.",{"title":61,"body":62},"Prepare environment variables","Create a `.env.production` file with at minimum:\n\n```bash\nPAYLOAD_SECRET=your-secret-32-characters-minimum\nDATABASE_URI=postgresql:\u002F\u002Fuser:pass@postgres:5432\u002Fpayload\nNEXT_PUBLIC_SERVER_URL=https:\u002F\u002Fexample.com\nNODE_ENV=production\nPAYLOAD_CONFIG_PATH=src\u002Fpayload.config.ts\n```\n\n`PAYLOAD_SECRET` signs session JWT tokens and encrypts API keys — a short or predictable value weakens the entire authentication system. Generate it with `openssl rand -hex 32`. `NEXT_PUBLIC_SERVER_URL` must match the final public URL: Payload uses it to build media links and email URLs.",{"title":64,"body":65},"Write Docker Compose with healthchecks","A robust `docker-compose.yml` includes healthchecks to ensure the app only starts after the database is ready:\n\n```bash\nservices:\n  app:\n    build: .\n    ports:\n      - \"127.0.0.1:3000:3000\"\n    env_file: .env.production\n    depends_on:\n      postgres:\n        condition: service_healthy\n    volumes:\n      - uploads:\u002Fapp\u002Fpublic\u002Fmedia\n\n  postgres:\n    image: postgres:16-alpine\n    environment:\n      POSTGRES_USER: payload\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n      POSTGRES_DB: payload\n    volumes:\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U payload\"]\n      interval: 5s\n      timeout: 5s\n      retries: 10\n\nvolumes:\n  pgdata:\n  uploads:\n```\n\nWithout the healthcheck and `service_healthy` condition, the app starts before PostgreSQL accepts connections, the connection pool fails and the container restart-loops.",{"title":67,"body":68},"Build the image and run migrations","Build then run migrations in this order:\n\n```bash\ndocker compose build\ndocker compose up -d postgres\ndocker compose run --rm app npx payload migrate\ndocker compose up -d app\n```\n\nFor PostgreSQL, `npx payload migrate` applies migration files generated in `src\u002Fmigrations\u002F`. If no files exist yet, generate them first with `npx payload migrate:create`. For MongoDB, the schema requires no migration: `migrate` only runs the data migrations you wrote yourself with `migrate:create`.",{"title":70,"body":71},"Configure Nginx as a reverse proxy","Point a vhost to `http:\u002F\u002F127.0.0.1:3000` (default Next.js port). Increase `client_max_body_size` for media uploads and add `X-Forwarded-Proto` headers so Payload generates correct HTTPS URLs.",{"title":73,"body":74},"Secure with SSL and set the server URL","Run `certbot --nginx -d example.com`, then verify that `serverURL: 'https:\u002F\u002Fexample.com'` is set in `payload.config.ts`. This URL drives media links, password reset emails and correct admin panel operation behind the proxy.",{"type":36,"title":76,"body":77},"Authentication and access management","Payload 3 manages users through the `users` collection defined in `payload.config.ts`. Enable authentication on the collection with `auth: true` — this automatically adds the `\u002Fapi\u002Fusers\u002Flogin`, `\u002Fapi\u002Fusers\u002Flogout`, `\u002Fapi\u002Fusers\u002Fme` and `\u002Fapi\u002Fusers\u002Frefresh-token` endpoints.\n\nFor roles, Payload does not impose a model: define a `role` field (type `select`) on the `users` collection, then drive access via `access` functions at the collection and operation level:\n\n```bash\naccess: {\n  read: ({ req: { user } }) => user?.role === 'admin',\n  create: isAdmin,\n  update: isAdminOrSelf,\n  delete: isAdmin,\n}\n```\n\nFor programmatic access (CI, third-party integrations), use **API keys**: enable `useAPIKey: true` in the collection's auth config. Users with admin access can then generate, regenerate or revoke a key from the admin panel. Pass it in the `Authorization: users API-Key your-key` header. The key is displayed only once: Payload encrypts it in the database with `PAYLOAD_SECRET` and never shows it again — a lost key must be regenerated, and changing `PAYLOAD_SECRET` invalidates all existing keys.",{"type":36,"title":79,"body":80},"Collection hooks: reacting to content events","Payload 3's **collection hooks** let you run code before or after each CRUD operation. They replace webhooks cleanly when logic lives in the same runtime:\n\n```bash\nhooks: {\n  afterChange: [\n    async ({ doc, operation }) => {\n      if (operation === 'create') {\n        await notifySubscribers(doc)\n      }\n    },\n  ],\n  beforeDelete: [\n    async ({ id }) => {\n      await cleanupMedia(id)\n    },\n  ],\n}\n```\n\nCollection hooks are `beforeOperation`, `beforeValidate`, `beforeChange`, `afterChange`, `beforeRead`, `afterRead`, `beforeDelete`, `afterDelete`, `afterOperation` and `afterError`; auth-enabled collections add, among others, `beforeLogin`, `afterLogin` and `afterLogout`. An `afterChange` hook is ideal for invalidating a CDN cache, sending a notification, or syncing with an external service after publication.\n\nFor **outbound HTTP webhooks** (notifying a static site, Slack, or a build pipeline), Payload has no dedicated module but `afterChange` hooks suffice: a simple `fetch` to your target endpoint inside the hook covers the use case. Avoid blocking the response on an external request — wrap the call in `setImmediate` or a background process if latency matters.",{"type":36,"title":82,"body":83},"Updates and schema migrations","Payload 3 follows semantic versioning. Patch updates (`3.x.y → 3.x.z`) are safe to apply without migrations. Minor updates (`3.x → 3.y`) may add columns or indexes and require running `payload migrate`.\n\nRecommended workflow for each update:\n\n```bash\n# 1. Bump the version in package.json\nnpm install payload@3.93.0 @payloadcms\u002Fdb-postgres@3.93.0\n\n# 2. Generate a migration file if the schema changed\nnpx payload migrate:create\n\n# 3. Test locally on a database copy\nnpx payload migrate\n\n# 4. Commit the migration file with the version bump\ngit add src\u002Fmigrations\u002F package.json package-lock.json\ngit commit -m \"chore: payload 3.93.0\"\n\n# 5. In production: stop app, apply migrations, restart\ndocker compose run --rm app npx payload migrate\ndocker compose up -d app\n```\n\nNever manually edit a migration in `src\u002Fmigrations\u002F` that has already been applied: Payload records each executed migration by name in the `payload-migrations` collection and does not run it again, so the edit never reaches production. Write a new migration instead. To roll back a migration: `npx payload migrate:down`.",{"type":36,"title":85,"body":86},"When `sharp` fails to load","This is the most common deployment obstacle on a VPS, and it takes three distinct forms. The most confusing is `Unsupported CPU: Prebuilt binaries for Linux x64 require v2 microarchitecture` (“linux-x64” before sharp 0.35): it comes from the **machine's processor**, not your code or dependencies. Pre-compiled binaries target an instruction set that older CPUs do not expose — this is a VPS selection criterion, not a bug to fix. The second, `Could not load the \"sharp\" module using the linux-x64 runtime`, almost always indicates a `node_modules` built on a different platform, typically a macOS development machine copied to a Linux server: reinstall on the target machine rather than copying the folder. The third is specific to multi-stage Docker: `sharp` installed in the build stage only, absent from the runtime stage — the container starts then fails on the first image operation. Install it explicitly in the final stage. On ARM architecture, the same vigilance applies: the library must be resolved for that platform.",{"type":36,"title":88,"body":89},"Troubleshooting: common production errors","**Build OOM — `Killed` or `JavaScript heap out of memory`.**\nOccurs during `npm run build` when the VPS is short on RAM and the Node.js heap reaches its limit. Set `NODE_OPTIONS=--max-old-space-size=3072` in the environment before the build, or build the image on a more powerful machine and push to a registry.\n\n**PostgreSQL — `Error: connect ECONNREFUSED 127.0.0.1:5432`.**\nThe app tries to connect before PostgreSQL is ready, or the connection URL points to `localhost` instead of the Docker service name (`postgres`). Verify that `DATABASE_URI` uses the service name (`postgresql:\u002F\u002Fpayload:pass@postgres:5432\u002Fpayload`) and that the healthcheck is in place (see step 3).\n\n**Admin inaccessible in prod — `\u002Fadmin` returns 404 or redirect loop.**\nTwo main causes: `serverURL` not set or not matching the real URL (Payload generates its internal redirects from this value), or the `X-Forwarded-Proto: https` header missing from the Nginx reverse proxy. Add `proxy_set_header X-Forwarded-Proto $scheme;` to the vhost and verify `serverURL` matches the public origin exactly, without a trailing slash.\n\n**CORS — `Access-Control-Allow-Origin` missing on the API.**\nPayload reads `cors` from `payload.config.ts`. In production, list your allowed origins explicitly: `cors: { origins: ['https:\u002F\u002Fexample.com'] }`. Leaving `cors: '*'` accepts all origins, which may be appropriate for a public API but exposes the admin to cross-origin requests. `cors: { origins: [serverURL] }` is the minimal safe setting.\n\n**An edited migration is not applied.**\nA file in `src\u002Fmigrations\u002F` was edited after it had been applied. Payload records each executed migration by name and never runs it again: the change is ignored without any error. Restore the original from Git, generate a new migration file if the schema changed, and apply it.",{"type":91,"title":92,"headers":93,"rows":97},"comparison","Payload CMS vs Strapi: which headless CMS to self-host?",[94,95,96],"Criterion","Payload CMS","Strapi",[98,102,106,110,114,118,122,126,130,134,138],[99,100,101],"Configuration approach","Code-first in TypeScript, versioned with Git","GUI-first via Content-Type Builder",[103,104,105],"Front integration","Native in Next.js (single runtime)","Decoupled, separate front and CMS",[107,108,109],"Supported databases","MongoDB, PostgreSQL and SQLite","PostgreSQL, MySQL, MariaDB, SQLite",[111,112,113],"Typing","Native end-to-end TypeScript","Generated types, looser integration",[115,116,117],"Server-side data access","Local API without HTTP calls","REST\u002FGraphQL API over HTTP",[119,120,121],"Content modeling","In code, by developers","In the interface, accessible to non-devs",[123,124,125],"RAM required for build","High (Next.js build)","High (React admin build)",[127,128,129],"Migration management","Versioned files, `payload migrate`","Schema synced at startup, migrations in `database\u002Fmigrations` (no CLI)",[131,132,133],"Native API keys","Yes, per collection, encrypted at rest","Yes, via API tokens",[135,136,137],"Collection hooks","Native, TypeScript-typed","Lifecycle hooks (`lifecycles.js`) and Document Service middlewares",[139,140,141],"Ideal for","Dev teams, typed Next.js projects","Mixed teams, visual modeling",{"type":143,"body":144},"tip","Use 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 eliminate an HTTP round-trip and gain both in latency and security. For media, plug in the `@payloadcms\u002Fstorage-s3` plugin to store uploads outside the VPS, and automate a daily database dump via a cron job for off-server backups. Finally, enable Payload's native **draft\u002Fpreview** workflow to offer editors a preview step before publishing, without any third-party plugin.","Deploy Payload CMS on a unified stack","The ServOrbit Cloud VPS offers the power needed for Payload's Next.js build and a Docker environment ready for MongoDB or PostgreSQL, to host your code-first CMS and your front end on the same machine.","Discover the Cloud VPS","\u002Fvps-cloud",[150,168,187],{"id":151,"slug":152,"slugs":153,"title":157,"excerpt":158,"readTime":23,"views":159,"isPinned":19,"noindex":19,"publishedAt":160,"updatedAt":161,"category":162,"categories":163,"featuredImage":30,"bgImage":31,"posterImage":165,"relatedSolution":166},5,"deploying-laravel-on-a-vps-a-production-guide",{"fr":154,"en":152,"ar":155,"es":156},"deployer-laravel-vps","نشر-laravel-على-خادم-vps-دليل-الإنتاج","desplegar-laravel-en-un-vps","Deploying Laravel on a VPS: a production guide","Take Laravel to production on a VPS: PHP, workers, cache, database, reverse proxy and HTTPS configured cleanly.",1,"2026-02-09T00:00:00+00:00","2026-09-07T11:26:10+00:00",{"id":23,"name":24,"slug":25,"color":26,"icon":27},[164],{"id":23,"name":24,"slug":25,"color":26,"icon":27},"\u002Fblog\u002Fcovers\u002Fdeployer-laravel-vps-poster.svg",{"categorySlug":27,"appSlug":167},"laravel-stack",{"id":169,"slug":170,"slugs":171,"title":175,"excerpt":176,"readTime":177,"views":159,"isPinned":19,"noindex":19,"publishedAt":178,"updatedAt":179,"category":180,"categories":181,"featuredImage":30,"bgImage":31,"posterImage":183,"relatedSolution":184},43,"deploy-a-nodejs-application-on-a-vps",{"fr":172,"en":170,"ar":173,"es":174},"deployer-nodejs-vps","نشر-تطبيق-nodejs-على-vps","desplegar-una-aplicacion-nodejs-en-un-vps","Deploy a Node.js Application on a VPS","Deploy a Node.js application to production on a VPS: PM2, Nginx reverse proxy, Let's Encrypt SSL, and automatic startup at boot.",8,"2026-05-08T00:00:00+00:00","2026-11-08T01:13:12+00:00",{"id":23,"name":24,"slug":25,"color":26,"icon":27},[182],{"id":23,"name":24,"slug":25,"color":26,"icon":27},"\u002Fblog\u002Fcovers\u002Fdeployer-nodejs-vps-poster.svg",{"categorySlug":185,"appSlug":186},"development","nodejs-stack",{"id":188,"slug":189,"slugs":190,"title":194,"excerpt":195,"readTime":196,"views":197,"isPinned":19,"noindex":19,"publishedAt":198,"updatedAt":199,"category":200,"categories":201,"featuredImage":30,"bgImage":31,"posterImage":203,"relatedSolution":204},44,"deploy-a-django-application-on-a-vps",{"fr":191,"en":189,"ar":192,"es":193},"deployer-django-vps","نشر-تطبيق-django-على-vps","desplegar-una-aplicacion-django-en-un-vps","Deploy a Django Application on a VPS","Deploy Django on a VPS: Gunicorn, Nginx, PostgreSQL, Docker, and SSL. A complete guide for Python developers who want to self-host.",3,7,"2026-05-07T00:00:00+00:00","2026-11-04T16:30:25+00:00",{"id":23,"name":24,"slug":25,"color":26,"icon":27},[202],{"id":23,"name":24,"slug":25,"color":26,"icon":27},"\u002Fblog\u002Fcovers\u002Fdeployer-django-vps-poster.svg",{"categorySlug":185,"appSlug":205},"django",true,1794270388989]