Strapi 5 tourne confortablement sur un Hetzner CCX13 (2 vCPU AMD dédiés, 8 Go de RAM, 80 Go NVMe) à environ 17 € HT par mois post-hausse d’avril 2026. C’est la configuration équilibrée pour un projet éditorial sérieux avec quelques milliers de contenus, des éditeurs qui chargent régulièrement des images, et une API consommée par un front-end Next.js ou Astro. Ce tutoriel pas à pas couvre le provisionnement du serveur, l’installation Docker, le déploiement Strapi avec PostgreSQL, la configuration du reverse-proxy Caddy avec HTTPS automatique, et les premières mesures de sécurisation.
L’objectif est qu’à la fin, votre instance Strapi soit accessible sur https://cms.votredomaine.com avec un certificat valide, sauvegardée automatiquement, et résistante au prochain redémarrage du serveur. Comptez deux à trois heures la première fois, une heure les suivantes une fois la procédure maîtrisée.
Prérequis
- Un compte Hetzner Cloud (création immédiate sur
console.hetzner.com, paiement par carte ou virement SEPA) - Un nom de domaine déjà acheté (Namecheap, OVH, Hosterra) avec accès au panneau DNS
- Une clé SSH publique générée localement avec
ssh-keygen -t ed25519 - Connaissances de base de la ligne de commande Linux (cd, ls, nano, sudo)
- Un projet Strapi 5 prêt à déployer (ou nous en créerons un dans les premières étapes)
Comptez environ 17 € pour le VPS, 1 à 2 € pour Object Storage si vous activez les médias S3, et autour de 10 € par an pour le nom de domaine. Le tutoriel utilise Ubuntu Server 24.04 LTS comme système d’exploitation : c’est l’image par défaut chez Hetzner, supportée jusqu’en 2029, et stable pour ce type de charge.
Étape 1 — Provisionner le serveur Hetzner
Connectez-vous à la console Hetzner et créez un nouveau projet (par exemple « cms-prod »). Avant de provisionner le serveur, ajoutez votre clé SSH publique dans « Security » → « SSH Keys » : Hetzner injectera cette clé dans le serveur au moment de la création, ce qui évite de gérer un mot de passe root par e-mail. Copiez le contenu de ~/.ssh/id_ed25519.pub dans le champ et donnez un nom parlant à la clé.
Cliquez ensuite sur « Add Server ». Choisissez la région la plus proche de votre audience : pour une cible francophone d’Afrique de l’Ouest, Falkenstein (Allemagne) offre la latence la plus stable, autour de 80 à 100 ms vers Dakar et Abidjan. Sélectionnez l’image Ubuntu 24.04. Choisissez le type « Dedicated vCPU » puis le plan CCX13. Activez les sauvegardes (option « Backups » en bas du formulaire) : cela coûte 20 % de plus mais offre des snapshots quotidiens conservés sept jours, ce qui est inestimable lors d’une mise à jour ratée. Cochez votre clé SSH dans « SSH Keys ». Donnez un hostname parlant (cms-prod-01) puis cliquez « Create & Buy Now ».
Au bout d’une minute, le serveur affiche son adresse IPv4 publique. Notez-la, vous en aurez besoin tout au long du tutoriel. Testez l’accès SSH :
ssh root@VOTRE.IP.PUBLIQUE
La première connexion demande de vérifier l’empreinte du serveur — comparez-la rapidement avec celle affichée dans la console Hetzner (rubrique « Server Fingerprints ») pour être sûr que vous parlez bien à votre machine et pas à un MITM. Une fois connecté en root, vous voyez le prompt Ubuntu. Le serveur est nu, il faut maintenant le préparer.
Étape 2 — Mettre à jour et durcir le système
Avant tout déploiement, on applique les patchs de sécurité disponibles, on crée un utilisateur dédié pour éviter d’opérer en root, et on active le pare-feu. Cinq minutes bien investies qui réduisent considérablement la surface d’attaque.
apt update && apt upgrade -y
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
Ces commandes mettent à jour tous les paquets, créent un utilisateur deploy avec privilèges sudo, copient votre clé SSH pour qu’il puisse se connecter, et fixent les permissions correctes sur le dossier .ssh. Testez la connexion en tant que deploy depuis un autre terminal : ssh deploy@VOTRE.IP. Si cela fonctionne, on peut interdire la connexion SSH en root, qui est la première cible des bots de scan.
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart ssh
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
Les deux sed désactivent la connexion root et l’authentification par mot de passe — désormais, seule votre clé privée donne accès. Le redémarrage de SSH applique les changements. Le pare-feu UFW bloque tout trafic entrant sauf SSH (port 22), HTTP (80) et HTTPS (443). Vérifiez que vous êtes toujours connecté avant de fermer la session, sinon vous risquez d’être verrouillé dehors. Si tout va bien, déconnectez-vous et reconnectez-vous en tant que deploy pour la suite.
Étape 3 — Installer Docker et Docker Compose
Strapi sera déployé en conteneur Docker, ce qui isole l’environnement, simplifie les mises à jour et rend le déploiement reproductible. Installer Docker depuis les paquets officiels Docker (et non depuis les paquets Ubuntu, qui sont souvent en retard de plusieurs versions) garantit la compatibilité avec les images Strapi récentes.
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker deploy
newgrp docker
docker --version
docker compose version
Le script get.docker.com est maintenu par Docker Inc., il détecte la distribution Ubuntu et installe le moteur Docker, le CLI et le plugin Compose. Ajouter deploy au groupe docker permet d’exécuter docker sans sudo, à condition de relancer la session de groupe avec newgrp docker. Les deux dernières commandes affichent la version installée : vous devez voir Docker 28+ et Compose v2.30+ en mai 2026, ce qui couvre largement les besoins de Strapi 5.
Étape 4 — Créer la structure du projet sur le serveur
L’arborescence que nous allons utiliser sépare le code de l’application, les volumes persistants (base et uploads) et la configuration du reverse-proxy. Cette séparation simplifie les sauvegardes et les mises à jour : on peut détruire et recréer un conteneur sans toucher aux données.
cd /home/deploy
mkdir -p strapi/{caddy,postgres-data,strapi-uploads}
cd strapi
Le dossier caddy contiendra la configuration du reverse-proxy. Les dossiers postgres-data et strapi-uploads serviront de volumes persistants pour la base PostgreSQL et les médias uploadés. Cette organisation permet de cibler précisément les sauvegardes : tar czf backup.tar.gz postgres-data strapi-uploads capture l’essentiel.
Étape 5 — Générer le projet Strapi et son Dockerfile
Strapi ne publie pas d’image Docker officielle — l’éditeur considère que chaque projet a ses spécificités et fournit à la place un outil communautaire qui génère le Dockerfile et le docker-compose.yml adaptés à votre projet. Cet outil est @strapi-community/dockerize, maintenu activement et recommandé dans la documentation officielle. On commence donc par créer un projet Strapi vierge dans un dossier app/, puis on lance l’outil pour générer la conteneurisation.
Pour l’installation initiale, Strapi exige Node.js 20, 22 ou 24 LTS. On va l’installer rapidement via nvm pour ne pas polluer le système, puis créer le projet en mode non interactif avec PostgreSQL comme client de base.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22
node --version
cd /home/deploy/strapi
npx create-strapi@latest app --dbclient=postgres --no-run --skip-cloud
cd app
npx @strapi-community/dockerize@latest
La première commande installe nvm puis Node.js 22 LTS. create-strapi scaffolde un projet TypeScript dans app/ avec PostgreSQL préconfiguré ; les flags --no-run et --skip-cloud évitent les invites interactives sur le serveur. @strapi-community/dockerize détecte le projet existant et génère un Dockerfile multi-stage optimisé (build puis runtime) ainsi qu’un docker-compose.yml de base. Répondez aux prompts : production, PostgreSQL, exposer 1337. À la fin, vous avez un Dockerfile dans app/ prêt à être construit.
Étape 6 — Adapter le docker-compose.yml pour la production
Le docker-compose.yml généré par dockerize est un bon point de départ mais vise un usage local. Pour la production, il faut ajouter Caddy comme reverse-proxy HTTPS, isoler les conteneurs sur un réseau interne, persister les volumes au bon endroit, et n’exposer que les ports 80 et 443. Remplacez le contenu généré par cette version durcie :
cat > /home/deploy/strapi/docker-compose.yml <<'EOF'
services:
postgres:
image: postgres:18-alpine
restart: unless-stopped
environment:
POSTGRES_USER: strapi
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: strapi
volumes:
- ./postgres-data:/var/lib/postgresql/data
networks:
- internal
strapi:
build: ./app
restart: unless-stopped
environment:
DATABASE_CLIENT: postgres
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_NAME: strapi
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: ${DB_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET}
APP_KEYS: ${APP_KEYS}
API_TOKEN_SALT: ${API_TOKEN_SALT}
NODE_ENV: production
volumes:
- ./strapi-uploads:/opt/app/public/uploads
depends_on:
- postgres
networks:
- internal
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- strapi
networks:
- internal
networks:
internal:
volumes:
caddy_data:
caddy_config:
EOF
Trois points méritent l’attention. Le service postgres utilise PostgreSQL 18 en variante Alpine pour économiser environ 250 Mo d’image disque. Le service strapi est construit depuis le dossier app/ via build: ./app, qui consommera le Dockerfile généré à l’étape précédente ; les secrets viennent de variables d’environnement (qu’on définira juste après) plutôt que d’être codés en dur. Le service caddy est le seul à exposer des ports vers l’extérieur ; il sert de point d’entrée unique. Le réseau internal permet aux conteneurs de se parler par nom (postgres, strapi) sans exposer la base au monde.
Étape 7 — Générer les secrets et le fichier .env
Strapi 5 exige plusieurs secrets pour fonctionner : mot de passe PostgreSQL, JWT secret pour les sessions utilisateur, JWT admin secret pour le panneau, deux clés d’application qui chiffrent les cookies, et un sel pour les tokens API. Tous doivent être aléatoires et longs. La commande openssl rand -base64 32 génère 32 octets aléatoires encodés en Base64, ce qui suffit largement.
cat > .env <<EOF
DB_PASSWORD=$(openssl rand -base64 24)
JWT_SECRET=$(openssl rand -base64 32)
ADMIN_JWT_SECRET=$(openssl rand -base64 32)
APP_KEYS=$(openssl rand -base64 32),$(openssl rand -base64 32)
API_TOKEN_SALT=$(openssl rand -base64 32)
EOF
chmod 600 .env
Le here-doc remplit le fichier .env avec des valeurs aléatoires fraîches. Le chmod 600 restreint la lecture au propriétaire — important si plusieurs utilisateurs partagent le serveur, même si dans notre cas seul deploy opère. Vérifiez le contenu avec cat .env : chaque ligne doit contenir une chaîne aléatoire de 30 à 50 caractères. Sauvegardez ce fichier dans un gestionnaire de mots de passe : en cas de panne du serveur, restaurer la base sans ces secrets est impossible.
Étape 8 — Configurer Caddy avec HTTPS automatique
Caddy obtient automatiquement un certificat Let’s Encrypt dès qu’on lui indique un nom de domaine, à condition que les DNS pointent vers l’IP du serveur. Avant cette étape, ouvrez le panneau de votre registrar et créez un enregistrement A pour cms.votredomaine.com qui pointe vers l’IP publique du serveur Hetzner. La propagation DNS peut prendre de cinq minutes à deux heures : vérifiez avec dig cms.votredomaine.com +short qui doit renvoyer votre IP avant de continuer.
cat > caddy/Caddyfile <<'EOF'
cms.votredomaine.com {
reverse_proxy strapi:1337
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
Remplacez cms.votredomaine.com par votre vrai nom de domaine partout dans le fichier. La directive reverse_proxy strapi:1337 renvoie tout le trafic vers le conteneur Strapi sur son port interne. Les en-têtes de sécurité ajoutent une couche de défense : HSTS impose HTTPS pendant un an, X-Content-Type-Options bloque le sniffing MIME, X-Frame-Options empêche le clickjacking. La compression gzip et zstd réduit la bande passante de 60 à 80 % sur les réponses JSON.
Étape 9 — Premier build et création de l’admin
Tout est en place pour le démarrage initial. Docker Compose va télécharger PostgreSQL et Caddy, construire l’image Strapi à partir du Dockerfile du dossier app/, créer les conteneurs, démarrer les services dans l’ordre, et obtenir le certificat HTTPS. Le premier build de Strapi prend cinq à dix minutes selon la bande passante (téléchargement des dépendances Node) ; les suivants seront beaucoup plus rapides grâce au cache de couches Docker.
docker compose up -d --build
docker compose logs -f strapi
Le -d (detached) lance les conteneurs en arrière-plan ; le flag --build reconstruit l’image Strapi si le code a changé. docker compose logs -f strapi suit en direct les journaux du conteneur Strapi. Vous verrez d’abord les messages de démarrage, puis l’application des migrations PostgreSQL (création des tables internes Strapi), puis le message « Welcome back! » avec l’URL d’accès. Faites Ctrl+C pour quitter le suivi de logs sans arrêter le service. Ouvrez maintenant https://cms.votredomaine.com/admin dans votre navigateur.
Une page d’inscription du premier administrateur s’affiche. Renseignez votre prénom, nom, e-mail et un mot de passe robuste de douze caractères au minimum. Validez. Vous arrivez sur le tableau de bord Strapi. C’est gagné : le serveur est en ligne, HTTPS fonctionne, la base est connectée, vous pouvez commencer à créer vos collections via le Content-Type Builder.
Étape 10 — Sauvegardes automatiques
Hetzner propose des sauvegardes quotidiennes complètes du VPS pour 20 % du prix mensuel — option à activer si ce n’est pas déjà fait dans la console. Cette sauvegarde système couvre 99 % des cas mais ne permet pas de restaurer un seul enregistrement perdu ; pour cela, on ajoute un dump PostgreSQL périodique envoyé sur Hetzner Object Storage ou un serveur distant.
cat > /home/deploy/strapi/backup-db.sh <<'EOF'
#!/bin/bash
set -e
DATE=$(date +%Y%m%d-%H%M)
docker compose exec -T postgres pg_dump -U strapi strapi | gzip > "/home/deploy/strapi/backups/db-${DATE}.sql.gz"
find /home/deploy/strapi/backups -name "db-*.sql.gz" -mtime +14 -delete
EOF
chmod +x /home/deploy/strapi/backup-db.sh
mkdir -p /home/deploy/strapi/backups
crontab -l 2>/dev/null | { cat; echo "0 3 * * * cd /home/deploy/strapi && ./backup-db.sh"; } | crontab -
Le script backup-db.sh exécute pg_dump dans le conteneur PostgreSQL, redirige le SQL vers gzip pour économiser l’espace, et écrit le fichier compressé dans backups/. La commande find … -mtime +14 -delete supprime les sauvegardes de plus de quatorze jours pour ne pas saturer le disque. Le crontab planifie l’exécution chaque nuit à 3 h du matin. Pour tester immédiatement, lancez ./backup-db.sh manuellement : un fichier db-AAAAMMJJ-HHMM.sql.gz doit apparaître dans backups/. Pour aller plus loin, ajoutez une commande rclone copy à la fin du script qui envoie la sauvegarde sur un Object Storage hors-site.
Étape 11 — Vérification finale
Trois tests rapides confirment que l’installation est saine. D’abord, l’accès au panneau d’administration via HTTPS : ouvrez https://cms.votredomaine.com/admin, le navigateur affiche un cadenas vert, vous pouvez vous connecter. Ensuite, l’API publique : créez une collection « test » avec un seul champ texte, ajoutez un enregistrement, autorisez la lecture publique dans « Settings → Roles → Public », puis testez avec curl.
curl https://cms.votredomaine.com/api/tests | head -c 300
La réponse JSON doit contenir un tableau data avec votre enregistrement. Si la réponse est {"data":[],"meta":...} alors que vous avez bien créé un contenu, vérifiez que vous avez activé « Find » et « FindOne » dans le panneau des permissions du rôle public. Troisième test, le redémarrage : sudo reboot. Au bout d’une minute, le serveur revient, et tous les services Docker redémarrent automatiquement grâce à restart: unless-stopped. Le site doit être de nouveau accessible sans intervention manuelle.
Erreurs fréquentes et solutions
| Erreur | Cause | Solution |
|---|---|---|
| Caddy ne peut pas obtenir de certificat | DNS pas encore propagé ou A record incorrect | dig cms.votredomaine.com ; attendre 30 min |
| Strapi crash avec OOM killed | RAM insuffisante (CX22 trop juste) | Passer au CCX13 (8 Go) via la console Hetzner |
relation "strapi_database_schema" does not exist | Migration interrompue au premier démarrage | docker compose down -v puis relancer (perd les données !) |
| Upload d’image échoue silencieusement | Permissions sur strapi-uploads | chmod -R 755 strapi-uploads && chown -R 1000:1000 |
| Latence très élevée depuis l’Afrique | Région Hetzner mal choisie | Recréer en Falkenstein (FSN1) plutôt que Nuremberg |
Mises à jour et maintenance
Strapi publie environ une version mineure par mois. Pour appliquer une mise à jour, on met à jour la dépendance dans app/package.json avec cd app && npx @strapi/upgrade latest, puis on reconstruit et redémarre le service : docker compose build strapi && docker compose up -d strapi. Les migrations de base sont appliquées automatiquement au démarrage du conteneur. Pour les versions majeures (5.x → 6.0 par exemple), lisez les release notes au préalable et testez sur un environnement de staging avant de toucher la production.
Pour le système hôte, sudo apt update && sudo apt upgrade -y une fois par mois suffit. Activer les mises à jour de sécurité automatiques avec sudo apt install unattended-upgrades évite d’oublier les patchs critiques. Surveillez la consommation mémoire avec docker stats : si Strapi dépasse régulièrement 6 Go, c’est le signal qu’il faut soit séparer la base et l’application sur deux VPS, soit passer au CCX23.
Tutoriels associés
- Article principal : Strapi, Directus, Payload — choisir et déployer en 2026
- Installer Payload CMS 3 dans Next.js
- Migrer un blog WordPress vers Strapi
Ressources officielles
- Documentation Strapi 5 — déploiement : docs.strapi.io/cms/deployment
- Image Docker officielle Strapi : hub.docker.com/r/strapi/strapi
- Hetzner Cloud — General Purpose : hetzner.com/cloud/general-purpose
- Documentation Caddy : caddyserver.com/docs
- Documentation PostgreSQL 18 : postgresql.org/docs/18
- Hetzner Object Storage : hetzner.com/storage/object-storage