Pourquoi self-héberger Payload CMS sur un VPS
Payload CMS adopte une approche radicalement code-first : votre schéma de contenu est défini en TypeScript dans des fichiers de configuration, versionnés avec Git, et depuis sa version 3 Payload s'installe directement dans une application Next.js via le App Router. Cela signifie qu'un VPS vous permet d'héberger le CMS et le site front dans un seul et même processus Node, partageant le build et le runtime. Vous obtenez un typage de bout en bout, des migrations de schéma gérées dans le code, et aucune dépendance à une interface graphique pour modéliser vos contenus. Le self-hosting est ici l'option naturelle : Payload est pensé pour être déployé comme n'importe quelle app Next.js, et un VPS vous donne le contrôle de la base (MongoDB ou PostgreSQL), des uploads et des variables d'environnement, sans plateforme intermédiaire.
Bénéfices concrets
- Schéma de contenu défini en TypeScript et versionné avec Git : revue de code et historique complets
- CMS et front Next.js dans un seul runtime : un seul build, un seul process à déployer
- Typage de bout en bout entre la config Payload, l'API et le front, sans génération manuelle
- Choix de la base : MongoDB ou PostgreSQL via les adaptateurs officiels
- Migrations de schéma pilotées par le code (
payload migrate), reproductibles entre environnements - Local API : accès direct aux données sans appel HTTP depuis le code serveur Next.js
- Aucun coût de licence : Payload CMS 3 est open-source (MIT) — vous ne payez que le VPS
Prérequis matériels et logiciels
Payload reposant sur Next.js, le build est exigeant : prévoyez un VPS de 2 vCPU et 4 Go de RAM pour builder et faire tourner l'application confortablement, surtout si le front Next.js est conséquent. Installez Node.js 20 LTS, Docker et Docker Compose. Côté base, provisionnez MongoDB 7 ou PostgreSQL 16 selon l'adaptateur choisi (@payloadcms/db-mongodb ou @payloadcms/db-postgres). Pointez votre domaine vers l'IP du VPS. Comptez 15 Go de disque pour les node_modules, le build .next, les uploads et les sauvegardes de base.
Déploiement pas à pas
Choisir et configurer l'adaptateur de base
Dans
payload.config.ts, déclarez l'adaptateur :mongooseAdapterpour MongoDB oupostgresAdapterpour PostgreSQL, en lisant l'URL de connexion depuisDATABASE_URI. Ce choix est structurant : PostgreSQL impose des migrations, MongoDB est plus souple sur le schéma.Préparer les variables d'environnement
Créez un fichier
.env.productionavec au minimum :PAYLOAD_SECRET=votre-secret-32-caracteres-minimum DATABASE_URI=postgresql://user:pass@postgres:5432/payload NEXT_PUBLIC_SERVER_URL=https://example.com NODE_ENV=production PAYLOAD_CONFIG_PATH=src/payload.config.tsPAYLOAD_SECRETchiffre les tokens JWT et les cookies de session — une valeur courte ou prévisible fragilise l'ensemble de l'authentification. Générez-la avecopenssl rand -hex 32.NEXT_PUBLIC_SERVER_URLdoit correspondre à l'URL publique finale : Payload l'utilise pour construire les liens des médias et les URLs d'email.Écrire le Docker Compose avec healthchecks
Un
docker-compose.ymlrobuste inclut des healthchecks pour ne démarrer l'app qu'après que la base soit prête :services: app: build: . ports: - "127.0.0.1:3000:3000" env_file: .env.production depends_on: postgres: condition: service_healthy volumes: - uploads:/app/public/media postgres: image: postgres:16-alpine environment: POSTGRES_USER: payload POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: payload volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U payload"] interval: 5s timeout: 5s retries: 10 volumes: pgdata: uploads:Sans le healthcheck et la condition
service_healthy, l'app démarre avant que PostgreSQL n'accepte les connexions, le pool de connexion échoue et le conteneur redémarre en boucle.Builder l'image et exécuter les migrations
Lancez le build puis les migrations dans cet ordre :
docker compose build docker compose up -d postgres docker compose run --rm app npx payload migrate docker compose up -d appPour PostgreSQL,
npx payload migrateapplique les fichiers de migration générés danssrc/migrations/. Si aucun fichier n'existe encore, générez-les d'abord avecnpx payload migrate:create. Pour MongoDB, le schéma est appliqué automatiquement au premier démarrage — la commandemigratereste sans effet.Configurer Nginx en reverse proxy
Pointez un vhost vers
http://127.0.0.1:3000(port Next.js par défaut). Augmentezclient_max_body_sizepour les uploads de médias et ajoutez les en-têtesX-Forwarded-Protopour que Payload génère des URLs HTTPS correctes.Sécuriser avec SSL et fixer l'URL serveur
Exécutez
certbot --nginx -d example.com, puis vérifiez queserverURL: 'https://example.com'est bien défini danspayload.config.ts. Cette URL conditionne les liens des médias, les emails de réinitialisation et le bon fonctionnement du panneau admin derrière le proxy.
Authentification et gestion des accès
Payload 3 gère les utilisateurs via la collection users définie dans payload.config.ts. Activez l'authentification sur la collection avec le champ auth: true — cela ajoute automatiquement les endpoints /api/users/login, /api/users/logout, /api/users/me et /api/users/refresh-token.
Pour les rôles, Payload n'impose pas de modèle : vous définissez un champ role (type select) sur la collection users, puis vous pilotez les accès via les fonctions access au niveau de chaque collection et de chaque opération :
access: {
read: ({ req: { user } }) => user?.role === 'admin',
create: isAdmin,
update: isAdminOrSelf,
delete: isAdmin,
}Pour les accès programmatiques (CI, intégrations tierces), utilisez les API keys : activez useAPIKey: true dans la config d'authentification de la collection. Chaque utilisateur peut alors générer une clé depuis le panneau admin. Transmettez-la dans l'en-tête Authorization: users API-Key votre-cle. Les API keys sont hachées en base — une clé perdue ne se récupère pas, elle se régénère.
Hooks de collection : réagir aux événements de contenu
Les collection hooks de Payload 3 permettent d'exécuter du code avant ou après chaque opération CRUD. Ils remplacent avantageusement les webhooks quand la logique vit dans le même runtime :
hooks: {
afterChange: [
async ({ doc, operation }) => {
if (operation === 'create') {
await notifySubscribers(doc)
}
},
],
beforeDelete: [
async ({ id }) => {
await cleanupMedia(id)
},
],
}Les hooks disponibles sont beforeOperation, beforeValidate, beforeChange, afterChange, beforeRead, afterRead, beforeDelete et afterDelete. Un hook afterChange est idéal pour invalider un cache CDN, envoyer une notification ou synchroniser avec un service externe après publication.
Pour les webhooks HTTP sortants (notifier un site statique, un Slack, un pipeline de build), Payload ne fournit pas de module dédié mais les hooks afterChange suffisent : un simple fetch vers votre endpoint cible à l'intérieur du hook remplit ce rôle. Veillez à ne pas bloquer la réponse sur une requête externe — wrappez l'appel dans un setImmediate ou un processus séparé si la latence est un critère.
Mises à jour et migrations de schéma
Payload 3 suit le versionnement sémantique. Les mises à jour de patch (3.x.y → 3.x.z) sont sûres à appliquer sans migration. Les mises à jour mineures (3.x → 3.y) peuvent ajouter des colonnes ou des index et nécessitent de rejouer payload migrate.
Workflow recommandé pour chaque mise à jour :
# 1. Bumper la version dans package.json
npm install [email protected] @payloadcms/[email protected]
# 2. Générer le fichier de migration si le schéma a changé
npx payload migrate:create
# 3. Tester en local sur une copie de la base
npx payload migrate
# 4. Committer le fichier de migration avec le bump de version
git add src/migrations/ package.json package-lock.json
git commit -m "chore: payload 3.88.0"
# 5. En production : arrêter l'app, appliquer les migrations, redémarrer
docker compose run --rm app npx payload migrate
docker compose up -d appNe jamais modifier à la main les fichiers générés dans src/migrations/ : Payload les vérifie par hash. Un fichier retouché fait échouer migrate avec Error: Migration file has been modified since it was created. Pour revenir en arrière sur une migration : npx payload migrate:down.
Quand `sharp` refuse de se charger
C'est le premier obstacle du déploiement sur VPS, et il prend trois formes distinctes qu'il vaut mieux savoir distinguer. La plus déroutante est Unsupported CPU: prebuilt binaries for linux-x64 require v2 microarchitecture : elle ne vient ni de votre code ni de vos dépendances, mais du processeur de la machine. Les binaires précompilés visent un jeu d'instructions que les CPU les plus anciens n'exposent pas — c'est donc un critère de choix de VPS, pas un bug à corriger. La deuxième, Could not load the "sharp" module using the linux-x64 runtime, signale presque toujours un node_modules fabriqué sur une autre plateforme, typiquement un macOS de développement copié vers un serveur Linux : réinstallez sur la machine cible plutôt que de transporter le dossier. La troisième est propre à Docker multi-étapes : sharp installé dans l'étape de construction seulement, absent de l'étape d'exécution — le conteneur démarre puis échoue au premier traitement d'image. Installez-le explicitement dans l'étape finale. Sur une architecture ARM, la même vigilance s'applique : la bibliothèque doit être résolue pour cette plateforme.
Dépannage : erreurs fréquentes en production
Build OOM — Killed ou JavaScript heap out of memory.
Survient pendant npm run build quand la RAM disponible est inférieure à 3 Go. Node.js est limité à ~1,8 Go par défaut. Passez NODE_OPTIONS=--max-old-space-size=3072 dans l'environnement avant de lancer le build, ou buildez l'image sur une machine plus puissante et poussez l'image vers un registry.
PostgreSQL — Error: connect ECONNREFUSED 127.0.0.1:5432.
L'app tente de se connecter avant que PostgreSQL soit prêt, ou l'URL de connexion pointe sur localhost au lieu du nom du service Docker (postgres). Vérifiez que DATABASE_URI utilise le nom du service (postgresql://payload:pass@postgres:5432/payload) et que le healthcheck est en place (voir étape 3).
Admin inaccessible en prod — /admin répond 404 ou redirige en boucle.
Deux causes principales : serverURL non défini ou non correspondant à l'URL réelle (Payload génère ses redirections internes depuis cette valeur), ou le header X-Forwarded-Proto: https absent du reverse proxy Nginx. Ajoutez proxy_set_header X-Forwarded-Proto $scheme; dans le vhost et vérifiez que serverURL est identique à l'origine publique, sans barre oblique finale.
CORS — Access-Control-Allow-Origin manquant sur l'API.
Payload lit cors depuis payload.config.ts. En production, listez explicitement vos origines autorisées : cors: { origins: ['https://example.com'] }. Laisser cors: '*' accepte toutes les origines, ce qui peut être voulu pour une API publique mais expose l'admin aux requêtes cross-origin. cors: { origins: [serverURL] } est le réglage minimal sûr.
Error: Migration file has been modified since it was created.
Un fichier dans src/migrations/ a été édité manuellement après sa génération. Payload vérifie l'intégrité de chaque fichier. Restaurez l'original depuis Git, générez un nouveau fichier de migration si le schéma a changé, et réappliquez dans l'ordre.
Payload CMS vs Strapi : quel CMS headless self-héberger ?
Faites défiler le tableau
| Critère | Payload CMS | Strapi |
|---|---|---|
| Approche de configuration | Code-first en TypeScript, versionné avec Git | GUI-first via le Content-Type Builder |
| Intégration front | Native dans Next.js (un seul runtime) | Découplé, front et CMS séparés |
| Bases supportées | MongoDB et PostgreSQL | PostgreSQL, MySQL, SQLite |
| Typage | TypeScript de bout en bout natif | Types générés, intégration moins serrée |
| Accès aux données serveur | Local API sans appel HTTP | API REST/GraphQL via HTTP |
| Modélisation du contenu | Dans le code, par les développeurs | Dans l'interface, accessible aux non-dev |
| RAM nécessaire au build | Élevée (build Next.js) | Élevée (build admin React) |
| Gestion des migrations | Fichiers versionnés, `payload migrate` | Migrations auto via Strapi CLI |
| API keys natives | Oui, par collection avec hachage | Oui, via les tokens d'API |
| Hooks de collection | Natifs, typés TypeScript | Lifecycle hooks via module middleware |
| Idéal pour | Équipes dev, projets Next.js typés | Équipes mixtes, modélisation visuelle |
Exploitez la Local API de Payload dans vos composants serveur Next.js : au lieu d'appeler votre propre API via fetch, importez getPayload et interrogez la base directement (payload.find({ collection: 'posts' })). Vous supprimez un aller-retour HTTP et gagnez en latence comme en sécurité. Pour les médias, branchez le plugin @payloadcms/storage-s3 afin de stocker les uploads hors du VPS, et automatisez un dump quotidien de votre base via un cron pour garantir des sauvegardes hors-serveur. Enfin, activez le draft/preview natif de Payload pour offrir un workflow éditorial de prévisualisation avant publication, sans aucun plugin tiers.