Strapi Cloud ou auto-hébergement : ce que vous perdez, ce que vous gagnez
Strapi Cloud est pratique pour démarrer, mais il impose des contraintes qui deviennent vite bloquantes en production réelle. Le plan Free limite les types de contenu et les utilisateurs admin ; les plans payants facturent au nombre de requêtes API et d'uploads. Surtout, vos données résident sur l'infrastructure de Strapi Inc. — ce qui est incompatible avec les exigences de localisation de données de nombreux clients (RGPD strict, contrats sectoriels, données sensibles). En self-hébergeant sur un VPS, vous récupérez la propriété complète de la base PostgreSQL, le choix du stockage (disque local ou bucket compatible S3), la liberté d'installer n'importe quel plugin de la communauté, et des volumes de contenu qui ne dépendent plus d'une grille tarifaire. La contrepartie est opérationnelle — vous gérez les mises à jour, les sauvegardes et le monitoring. Ce guide vous montre exactement comment faire.
Bénéfices de l'auto-hébergement de Strapi sur un VPS
- Propriété des données — votre base PostgreSQL et vos uploads restent sur votre VPS, sous votre contrôle exclusif
- Aucun quota de plan — le nombre d'entrées, de types de contenu et de comptes admin n'est plus fixé par un abonnement, mais par les ressources de votre VPS
- Plugins communautaires — tout plugin npm s'installe librement, sans validation ni liste blanche de la plateforme
- API sur votre domaine — REST et GraphQL exposés sur
cms.mondomaine.com, sans intermédiaire ni limite de débit imposée - Pile unifiée — Strapi et un frontend Nuxt ou Next.js cohabitent sur le même VPS, derrière un reverse proxy unique
- Localisation des données — essentielle pour la conformité RGPD et les exigences contractuelles de vos clients
- Coût prévisible — le VPS a un tarif fixe mensuel, indépendant du volume de contenu ou du trafic API
- Contrôle des migrations — vous choisissez quand et comment appliquer les mises à jour de Strapi
Prérequis : matériel, logiciels et domaine
Strapi est plus gourmand que la plupart des frameworks car le build de l'admin React consomme plus de 2 Go de RAM à lui seul. Un VPS de 1 Go de RAM échouera systématiquement en OOM pendant la phase de build — ne sous-dimensionnez pas. Cibles minimales : 2 vCPU, 4 Go de RAM. En production stable, une fois le build terminé, l'application tourne confortablement avec 512 Mo à 1 Go. Sur le plan logiciel : Docker 24+ et Docker Compose v2 (docker compose, pas docker-compose), Git pour récupérer votre code sur le serveur, et Certbot pour le TLS. Côté base de données : PostgreSQL 16 est recommandé en production — SQLite est fonctionnel en développement mais trop limité sous charge concurrente et incompatible avec certains plugins. Côté réseau : pointez cms.mondomaine.com vers l'IP de votre VPS avant de commencer, et prévoyez 20 Go de disque minimum (node_modules, uploads, dumps de sauvegarde).
Préparer le projet avant de toucher au serveur
Générer le projet Strapi
Sur votre poste, générez le projet avec la commande officielle
npx create-strapi-app@latest mon-cms --dbclient=postgres. Choisissez TypeScript si votre équipe le maîtrise. Initialisez le dépôt Git, poussez-le sur votre forge, puis clonez-le sur le VPS dans/srv/mon-cms. Tout ce qui suit se prépare dans ce dépôt et non sur le serveur : le VPS ne doit recevoir que du code déjà versionné.Brancher la configuration de base de données sur PostgreSQL
Dans
config/database.js— ouconfig/database.tsen TypeScript —, la fonction exportée reçoitenvet renvoie un objetconnection. Déclarezclient: 'postgres', puis six clés lues depuis l'environnement dans le sous-objetconnection:host: env('DATABASE_HOST', '127.0.0.1'),port: env.int('DATABASE_PORT', 5432),database: env('DATABASE_NAME', 'strapi'),user: env('DATABASE_USERNAME', 'strapi'),password: env('DATABASE_PASSWORD', '')etssl: env.bool('DATABASE_SSL', false). Le second argument deenv()n'est qu'un repli de développement : aucune valeur réelle ne doit figurer dans ce fichier, il part dans Git.Générer les cinq secrets de production
Strapi refuse de démarrer en production sans
APP_KEYS,API_TOKEN_SALT,ADMIN_JWT_SECRET,JWT_SECRETetTRANSFER_TOKEN_SALT. Générez chacun avecopenssl rand -base64 32;APP_KEYSen attend plusieurs, séparées par des virgules, donc produisez-en au moins deux. Écrivez-les dans/srv/mon-cms/.envsur le VPS — jamais dans Git — avecDATABASE_HOST,DATABASE_NAME,DATABASE_USERNAME,DATABASE_PASSWORD,NODE_ENV=productionetURL=https://cms.mondomaine.com. Une clé manquante se traduit par un refus de démarrage, pas par un avertissement.Écrire le Dockerfile multi-étages
Deux étages suffisent à garder l'image de production légère. L'étage
builderpart denode:20-alpine, fixeWORKDIR /app, copiepackage*.json, lancenpm ci, copie le reste du code, puis compile l'admin avecNODE_ENV=production npm run build. L'étagerunnerrepart de la même image de base et ne recopie dubuilderque trois choses : le dossier de build,node_modulesetpackage.json. Terminez parEXPOSE 1337et unCMDqui appellenpm run start— c'est le script de démarrage que Strapi installe, n'essayez pas de lancer un fichier serveur à la main. Si le build meurt en OOM, posezNODE_OPTIONS=--max-old-space-size=4096avantnpm run build.
Déployer la pile sur le VPS
Décrire les services dans docker-compose.yml
Le fichier déclare deux services et un volume nommé. Le service
postgresutilise l'imagepostgres:16-alpine, reçoitPOSTGRES_DB,POSTGRES_USERetPOSTGRES_PASSWORDdans son blocenvironment, monte le volumepgdatasur/var/lib/postgresql/dataet passe enrestart: unless-stopped. Le servicestrapise construit depuis le Dockerfile local avecbuild: ., lit ses variables parenv_file: .env, déclaredepends_on: postgres, monte./public/uploadssur/app/public/uploadset publie son port avecports: 127.0.0.1:1337:1337. Cette liaison sur la boucle locale est le point important : Nginx devient le seul point d'entrée public, le port 1337 n'est jamais exposé à l'extérieur. Un serviceredisreste optionnel, pour le cache de sessions ou les files de traitement.Construire l'image et démarrer la pile
Lancez
docker compose build, puisdocker compose up -d. Suivez le démarrage avecdocker compose logs -f strapi. Strapi applique ses migrations de schéma au premier démarrage dès queNODE_ENV=productionest posé. Comptez deux à cinq minutes : la construction de l'admin React est de loin l'étape la plus lente. Attendez la ligneStrapi started successfullyavant de continuer.Placer Nginx en reverse proxy
Créez le vhost
/etc/nginx/sites-available/cms.mondomaine.com. Il écoute enlisten 80surserver_name cms.mondomaine.com, porte unclient_max_body_size 50M— indispensable pour les uploads de médias — et un unique bloclocation /qui faitproxy_pass http://127.0.0.1:1337. Ajoutez-y les quatre en-têtes que Strapi attend derrière un proxy :Host,X-Real-IP,X-Forwarded-ForetX-Forwarded-Proto, chacun posé par une directiveproxy_set_header. Activez le vhost par un lien symbolique verssites-enabled, puis validez et rechargez avecnginx -t && systemctl reload nginx.Activer HTTPS et fixer la variable URL
Obtenez le certificat avec
certbot --nginx -d cms.mondomaine.com: Certbot réécrit le vhost pour rediriger HTTP vers HTTPS. Vérifiez ensuite queURL=https://cms.mondomaine.comfigure bien dans le.env, sans barre oblique finale. C'est cette variable que Strapi utilise pour construire les liens des médias et les redirections de l'admin ; sans elle, les fichiers téléversés sortent avec des adresses fausses et le panneau d'administration se comporte mal derrière le proxy. Redémarrez le conteneur après toute modification du.env: les variables sont lues au démarrage.Vérifier que l'instance répond vraiment
Ouvrez
https://cms.mondomaine.com/adminet créez le premier compte administrateur — Strapi le demande au premier accès et ne le redemandera plus. Contrôlez ensuite trois points :docker compose psmontre les deux conteneurs en étatrunning, l'API publique répond sur/api, et l'envoi d'un fichier de test depuis la médiathèque aboutit. Un502 Bad Gatewayà ce stade signale presque toujours un conteneurstrapiarrêté ou encore en cours de build : relisezdocker compose logs strapiavant de toucher à Nginx.
Sortir les médias du disque : le provider S3
Par défaut, Strapi range les fichiers téléversés dans public/uploads, sur le disque du VPS. Le montage ./public/uploads du docker-compose les fait survivre à une recréation de conteneur, mais ils grossissent avec la médiathèque, entrent dans chaque sauvegarde et disparaissent avec le serveur. Le provider officiel règle les trois problèmes : npm install @strapi/provider-upload-aws-s3. Déclarez-le ensuite dans config/plugins.js, sous la clé upload puis config : provider: 'aws-s3' et un objet providerOptions qui lit quatre valeurs de l'environnement — accessKeyId: env('AWS_ACCESS_KEY_ID'), secretAccessKey: env('AWS_ACCESS_SECRET'), region: env('AWS_REGION') et params: { Bucket: env('AWS_BUCKET') }. N'importe quel stockage compatible S3 convient : Scaleway Object Storage, Wasabi, Cloudflare R2. Deux pièges méritent d'être connus avant la bascule. Les médias déjà téléversés ne migrent pas tout seuls — basculez le provider avant la mise en ligne, ou copiez le contenu de public/uploads vers le bucket à la main. Et les URLs des médias changent de domaine : si votre frontend les met en cache ou les réécrit, vérifiez-le après la bascule.
Automatiser la sauvegarde de la base
Créer le dossier de destination
Sur le VPS,
mkdir -p /srv/backups/strapi. Gardez ce dossier hors du dépôt Git et hors de tout chemin monté dans un conteneur : un dump ne doit jamais partir dans une image ni dans une release.Repérer le nom réel du conteneur PostgreSQL
docker compose psdonne le nom exact, de la formemon-cms-postgres-1. Il dérive du nom du dossier de projet : ne le recopiez pas d'un guide, lisez-le sur votre machine. Un script de sauvegarde qui vise un conteneur inexistant échoue en silence une fois placé dans le cron.Écrire la commande de dump
Une seule ligne suffit :
docker exec mon-cms-postgres-1 pg_dump -U strapi strapi | gzip > /srv/backups/strapi/strapi-$(date +%Y%m%d).sql.gz. Lepg_dumps'exécute dans le conteneur, la compression et l'écriture se font sur l'hôte. Lancez-la une première fois à la main et vérifiez la taille du fichier produit : un dump de quelques octets signale une erreur d'authentification avalée par le tube.Purger les dumps trop anciens
Ajoutez à la suite
find /srv/backups/strapi -name '*.sql.gz' -mtime +7 -delete. Sans cette ligne, le disque se remplit en quelques semaines : c'est la panne la plus banale d'une sauvegarde quotidienne — la base tombe parce que la sauvegarde a saturé le volume.Planifier, puis sortir les dumps du serveur
Placez les deux commandes dans un script
/etc/cron.daily/strapi-backup, avec#!/bin/bashen première ligne et unchmod +xpour le rendre exécutable. Ajoutez-y unrsyncvers un stockage externe, ou un envoi vers le même bucket S3 que les médias. Une sauvegarde qui reste sur la machine qu'elle sauvegarde ne protège de rien ; testez une restauration complète au moins une fois avant d'en dépendre.
Mettre à jour Strapi sans casser les migrations
Une mise à jour se pilote depuis le dépôt, pas sur le serveur : changez la version dans package.json, poussez, puis, sur le VPS, enchaînez git pull et docker compose build && docker compose up -d. Strapi détecte et applique les migrations de schéma au démarrage tant que NODE_ENV=production est posé ; confirmez-le avec docker compose logs strapi | grep -i migrat. Trois précautions valent le détour. Prenez un pg_dump juste avant la montée de version, et pas seulement celui de la nuit précédente — une migration ratée se répare par une restauration, jamais par un second essai. Lisez les notes de version des plugins que vous avez installés : une montée majeure de Strapi casse plus souvent un plugin communautaire que le cœur lui-même. Enfin, si une migration échoue, ne relancez pas la pile en boucle : chaque redémarrage rejoue la même migration sur une base déjà à moitié modifiée. Arrêtez les conteneurs, lisez le journal complet, restaurez le dump si nécessaire, puis corrigez.
Dépannage : les erreurs les plus fréquentes
OOM pendant le build (Killed ou JavaScript heap out of memory) : le build de l'admin React dépasse la mémoire disponible. Deux remèdes — ajouter NODE_OPTIONS=--max-old-space-size=4096 dans l'étage builder du Dockerfile, ou augmenter temporairement le swap du VPS avec fallocate -l 2G /swapfile, chmod 600 /swapfile, mkswap /swapfile puis swapon /swapfile. Si le VPS reste bloqué, construisez l'image sur une machine plus puissante et poussez-la dans un registre.
Cannot find module @strapi/plugin-* : ne partagez jamais le dossier node_modules entre un environnement de développement local et l'image de production via un volume Docker. Le node_modules local est compilé pour votre système d'exploitation, pas pour l'Alpine Linux du conteneur. Supprimez tout volume node_modules du docker-compose et laissez le npm ci du Dockerfile s'en charger.
URL mismatch dans l'admin, ou médias en chemin relatif : la variable URL du .env doit correspondre exactement à l'adresse publique HTTPS de votre Strapi, sans barre oblique finale. Toute divergence casse les liens des médias téléversés et provoque des erreurs CORS dans le panneau d'administration.
413 Request Entity Too Large : la directive client_max_body_size de Nginx est trop basse. Passez-la à 50M au minimum, ou à 100M si vous téléversez des vidéos, puis rechargez Nginx.
password authentication failed for user au démarrage : le mot de passe du .env a changé après la création du volume pgdata. PostgreSQL ne relit son mot de passe qu'à l'initialisation du volume ; alignez le .env sur le mot de passe existant, ou repartez d'un volume neuf après avoir sauvegardé la base.
Trois règles non négociables pour une instance Strapi en production : S3 pour les uploads (@strapi/provider-upload-aws-s3) — vos médias survivent à toute recréation de conteneur et ne grossissent pas le disque du VPS ; pg_dump quotidien automatisé copié hors du serveur — une sauvegarde locale seule disparaît avec le VPS ; NODE_ENV=production impérativement — le mode développement recompile l'admin à chaud, expose le Content-Type Builder et désactive les optimisations de cache, à proscrire en production.
Strapi et un frontend Nuxt ou Next.js sur le même VPS
Il est tout à fait possible de faire cohabiter Strapi et un frontend sur le même VPS, à condition d'avoir suffisamment de RAM — comptez 8 Go pour les deux, puisque les deux builds peuvent se déclencher au même moment. L'architecture la plus simple utilise Nginx comme dispatcher : les requêtes vers cms.mondomaine.com sont proxiées vers le port 1337 (Strapi), et celles vers mondomaine.com vers le port 3000 (Nuxt ou Next.js). Strapi expose son API REST sur /api — le frontend la consomme directement sur le réseau Docker interne, sans repasser par Nginx, ce qui réduit la latence. Si votre frontend génère des pages statiques (nuxt generate, ou next build en export statique), Nginx peut servir les fichiers depuis le disque et ne proxier que les routes dynamiques. Cette architecture monolithique est idéale pour un projet de taille moyenne — un seul serveur, un seul certificat TLS, un seul point de supervision. Si vous préférez séparer le frontend, nos guides de déploiement Nuxt et Node.js reprennent la même méthode sur un second VPS Cloud dimensionné pour le build.