Auto-héberger Ghost avec MySQL
Suivez ça sur votre propre serveur. Commencer gratuitement, sans carte
L’installateur en une ligne de Ghost vous donne un blog fonctionnel rapidement, mais il suppose discrètement SQLite et une seule machine que vous ne déplacerez jamais. Dès que vous voulez MySQL, un second serveur pour les tâches de fond, ou simplement un fichier Docker Compose que vous pouvez lire et sauvegarder vous-même, vous êtes seul. Ce guide construit cette configuration de bout en bout : Ghost et MySQL comme deux conteneurs, Caddy devant pour le HTTPS, de vraies sauvegardes et une procédure de mise à jour qui ne demande pas de croiser les doigts. À la fin, vous aurez une publication qui tourne sur une infrastructure que vous comprenez, plus la version en un clic si vous préférez sauter le YAML.
Ce guide utilise la ligne stable actuelle de Ghost (Ghost 6.x) pour l’installation manuelle ci-dessous, et MySQL 8.0.
Ce qu’il vous faut
- Un serveur. Un Droplet Basic DigitalOcean à 2 vCPU et 2 Go de RAM fait tourner Ghost et MySQL confortablement pour 18 $US/mois; le droplet à 1 vCPU et 1 Go à 6 $US/mois est trop juste une fois que MySQL et Node sont tous les deux en mémoire. N’importe quelle machine Ubuntu 22.04 ou 24.04 fonctionne de la même façon.
- Un nom de domaine, avec son enregistrement DNS A déjà pointé vers l’IP du serveur avant l’étape du HTTPS.
- Un fournisseur SMTP (Postmark, SES, Mailgun, ou autre) pour les courriels transactionnels. Ghost en a besoin pour les invitations d’équipe, les réinitialisations de mot de passe et les liens de connexion des membres; ça n’a rien à voir avec l’envoi d’infolettres, que Ghost gère lui-même une fois le courriel configuré.
Étape 1 : pointer le DNS et préparer le serveur
Créez d’abord l’enregistrement A pour votre domaine, puisque l’émission du certificat plus tard dépend de sa résolution déjà en place :
dig +short blog.example.com
Vous devriez voir l’IP de votre serveur une fois la propagation terminée, généralement en quelques minutes chez la plupart des registraires.
Connectez-vous en SSH et installez Docker Engine et le plugin Compose :
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
Déconnectez-vous et reconnectez-vous pour que le changement de groupe prenne effet, puis confirmez :
docker compose version
Vous devriez voir une chaîne de version Compose (v2.x), pas une erreur « commande introuvable ».
Étape 2 : écrire le fichier Docker Compose
Créez un répertoire de projet et un docker-compose.yml avec trois services : Ghost, MySQL, et Caddy comme reverse proxy.
services:
ghost:
image: ghost:6-alpine
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
NODE_ENV: production
url: https://${DOMAIN:?set DOMAIN in .env}
database__client: mysql
database__connection__host: db
database__connection__port: "3306"
database__connection__user: ghost
database__connection__password: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}
database__connection__database: ghost
mail__transport: SMTP
mail__options__host: ${SMTP_HOST}
mail__options__port: "465"
mail__options__secure: "true"
mail__options__auth__user: ${SMTP_USER}
mail__options__auth__pass: ${SMTP_PASSWORD}
mail__from: ${MAIL_FROM}
volumes:
- ghost_content:/var/lib/ghost/content
networks:
- ghost_net
db:
image: mysql:8.0.44
restart: unless-stopped
environment:
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}
MYSQL_ROOT_PASSWORD: ${DATABASE_ROOT_PASSWORD:?set DATABASE_ROOT_PASSWORD in .env}
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${DATABASE_ROOT_PASSWORD}"]
interval: 5s
start_period: 30s
retries: 20
networks:
- ghost_net
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
- ghost
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- ghost_net
networks:
ghost_net:
volumes:
ghost_content:
mysql_data:
caddy_data:
caddy_config:
Les variables d’environnement à double tiret bas (database__connection__host) sont la convention propre à Ghost pour écraser sa config JSON imbriquée sans toucher à un fichier, documentée dans la référence de configuration de Ghost. url doit être la vraie adresse https:// dès le départ : Ghost l’intègre dans les liens générés et les thèmes, et la changer plus tard demande un ghost config complet et une reconstruction du cache plutôt qu’une simple modification.
À côté du fichier Compose, créez un .env avec de vraies valeurs :
DOMAIN=blog.example.com
DATABASE_PASSWORD=une-longue-chaine-aleatoire
DATABASE_ROOT_PASSWORD=une-autre-longue-chaine-aleatoire
SMTP_HOST=smtp.postmarkapp.com
SMTP_USER=votre-nom-utilisateur-smtp
SMTP_PASSWORD=votre-mot-de-passe-smtp
MAIL_FROM="Votre Blog <hello@blog.example.com>"
Générez les deux mots de passe plutôt que de les taper à la main :
openssl rand -hex 24
Vous devriez obtenir une chaîne hexadécimale de 48 caractères à chaque exécution; utilisez-en une pour DATABASE_PASSWORD et une seconde pour DATABASE_ROOT_PASSWORD.
Étape 3 : écrire le Caddyfile
Caddy demande et renouvelle le certificat automatiquement dès qu’il peut répondre sur les ports 80 et 443 pour votre domaine :
blog.example.com {
reverse_proxy ghost:2368
}
C’est tout le fichier. Le défi ACME de Caddy a besoin que le port 80 soit ouvert sur l’internet public même si le trafic final passe au HTTPS, alors ne le bloquez pas au pare-feu.
Étape 4 : démarrer la pile
docker compose up -d
docker compose logs -f ghost
Vous devriez voir le health check de MySQL passer, puis Ghost afficher une ligne comme Ghost is running in production... suivie de l’URL qu’il sert. Visitez https://blog.example.com/ghost pour arriver sur l’assistant de configuration et créer votre premier compte administrateur.
Étape 5 : sauvegarder MySQL et le volume de contenu
Les données réelles de Ghost vivent à deux endroits : MySQL (articles, membres, réglages) et le volume ghost_content (thèmes, images, téléversements). Sauvegardez les deux ensemble pour qu’une restauration reste cohérente :
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p backups
docker compose exec -T db mysqldump -u root -p"$DATABASE_ROOT_PASSWORD" ghost | gzip > "backups/ghost-db-$STAMP.sql.gz"
docker run --rm -v ghost_content:/content -v "$(pwd)/backups:/backup" alpine \
tar czf "/backup/ghost-content-$STAMP.tar.gz" -C /content .
Lancez-le depuis le même répertoire que votre fichier Compose (pour que ghost_content pointe vers le bon volume du projet) et mettez-le sur une tâche cron quotidienne. Copiez le répertoire backups/ hors du serveur lui-même, un rsync programmé vers une autre machine ou un petit compartiment de stockage objet suffit; une sauvegarde qui ne vit que sur le serveur qu’elle est censée protéger n’en est pas une.
Vous devriez voir deux nouveaux fichiers dans backups/ après chaque exécution, et leur taille devrait suivre grosso modo la croissance de votre contenu, pas rester à zéro.
Étape 6 : mises à jour
Tirez la nouvelle image et recréez seulement le conteneur Ghost; MySQL et son volume de données ne sont pas touchés :
docker compose pull ghost
docker compose up -d ghost
Ghost exécute ses propres migrations de base de données au démarrage, alors vérifiez les logs pour une ligne confirmant que la migration est terminée avant de considérer la mise à jour faite :
docker compose logs --tail=50 ghost
Prenez une sauvegarde (étape 5) avant chaque mise à jour, surtout d’une version majeure comme de 5 à 6, puisque c’est exactement le genre de changement où une migration peut échouer à mi-chemin.
Le raccourci : faites-le avec Railyard
Tout ce qui précède, les deux conteneurs, le câblage entre eux, le reverse proxy et le certificat, c’est ce que le modèle Ghost + MySQL de Railyard met en place pour vous depuis le catalogue de modèles. Il fait tourner ghost:5.130.6-alpine avec un mysql:8.0.39 en side-car : RAILYARD_URL et la chaîne de connexion MySQL sont câblées automatiquement, donc il n’y a pas de database__connection__host à taper à la main, et un volume ghost-content plus un volume ghost-db-data sont créés et sauvegardés sans qu’il y ait une tâche cron à écrire vous-même.
Ouvrez https://app.railyard.run/deploy?stack=ghost-mysql et choisissez Ghost + MySQL dans le catalogue. Vous ajoutez quand même vos propres identifiants SMTP pour le courriel (Railyard ne fournit pas de transport courriel), mais la base de données, les volumes et le certificat HTTPS sont déjà gérés. Voyez Bouton de déploiement Railyard si vous préférez publier le même flux en un clic depuis votre propre README, et Bases de données et sauvegardes pour savoir comment les sauvegardes programmées et les restaurations fonctionnent une fois que c’est en place.
Dépannage
Le port 80 ou 443 est déjà utilisé. Quelque chose d’autre (souvent une installation Apache ou nginx par défaut) est déjà lié. sudo ss -tlnp | grep -E ':80|:443' vous montre quoi, et il faut l’arrêter ou le déplacer avant que Caddy puisse lier ces ports.
Le certificat n’est jamais émis. Les logs de Caddy montreront des échecs ACME répétés. La cause quasi universelle est le DNS : dig +short blog.example.com doit retourner l’IP du serveur avant que Caddy puisse prouver la propriété à Let’s Encrypt. Vérifiez aussi que rien en amont (un pare-feu cloud, un groupe de sécurité par défaut du fournisseur) ne bloque le port 80 entrant.
Ghost redémarre en boucle et les logs montrent une erreur de connexion MySQL. database__connection__host doit correspondre au nom du service Compose (db dans le fichier ci-dessus), et le mot de passe dans l’environnement de Ghost doit être exactement la même chaîne que MYSQL_PASSWORD sur le service db. Un mot de passe avec un $ ou un # doit être mis entre guillemets de façon cohérente aux deux endroits, ou échappé, puisque Compose et le shell essaient tous les deux d’interpréter ces caractères.
Permission refusée en écrivant dans le volume de contenu. Ça apparaît après avoir restauré une sauvegarde prise avec tar en root dans un volume que le conteneur Ghost s’attend à posséder avec son propre utilisateur. Restaurez dans un volume nommé neuf avec docker run --rm -v ghost_content:/content alpine tar xzf /backup/ghost-content-TIMESTAMP.tar.gz -C /content plutôt que de copier des fichiers avec votre propre compte utilisateur.
MySQL manque de mémoire sur un serveur à 1 Go. Les réglages par défaut du buffer pool de MySQL s’ajustent à la machine, et sur un droplet à 1 Go ça laisse trop peu pour Ghost et Node. C’est pour ça que le droplet à 18 $US/mois et 2 Go ci-dessus est le vrai minimum, pas la taille la moins chère de la liste.
Foire aux questions
Puis-je utiliser SQLite à la place de MySQL? Oui, l’installation CLI par défaut de Ghost utilise encore SQLite, et c’est vraiment correct pour un site unique à faible trafic. MySQL mérite sa complexité une fois que vous voulez faire tourner plus d’une instance Ghost contre les mêmes données, ou que vous arrivez d’un hébergeur qui vous donnait déjà du MySQL géré.
Est-ce que ça supporte les infolettres et les fonctions de membership de Ghost? Oui, une fois que le SMTP (pour le courriel transactionnel) et un fournisseur d’envoi en masse (Ghost s’intègre avec Mailgun spécifiquement pour l’envoi d’infolettres) sont tous les deux configurés. Les deux sont des réglages distincts dans l’admin de Ghost, faciles à confondre puisqu’ils sont tous les deux « du courriel ».
Puis-je faire tourner Ghost sans Docker? Ghost supporte une installation native Node.js sur Ubuntu documentée sur docs.ghost.org, avec MySQL installé comme paquet système plutôt qu’en conteneur. Le chemin Docker de ce guide est plus facile à sauvegarder et à déplacer d’un serveur à l’autre, c’est pour ça qu’on l’a utilisé ici.
Comment déplacer un site Ghost existant vers cette configuration? Utilisez l’export/import propre à Ghost (Settings → Migration options) pour le contenu, ou restaurez un mysqldump directement dans la base de données du conteneur db si vous arrivez d’une autre installation sur MySQL, puis copiez l’ancien répertoire content/images dans le nouveau volume ghost_content.
Qu’arrive-t-il si le serveur redémarre? restart: unless-stopped sur chaque service fait en sorte que Docker remet les conteneurs en marche automatiquement dès que le démon Docker lui-même démarre, ce qui arrive au boot par défaut sur une distribution basée sur systemd comme Ubuntu.
Les deux chemins mènent à un site Ghost que vous contrôlez vraiment : le manuel si vous voulez voir chaque pièce bouger, le modèle si vous préférez commencer à écrire aujourd’hui.
Pourquoi les gens choisissent Railyard pour ça
- Votre propre compte infonuagique. Déployez sur DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou n'importe quel serveur Linux par SSH, facturé directement par votre fournisseur, sans majoration.
- Un prix fixe par équipe. L'offre gratuite est gratuite pour toujours : un serveur plus un serveur de base de données, trois applications, sans carte. Pro coûte 19 $ par mois pour jusqu'à cinq serveurs et un nombre illimité d'applications. Team coûte 49 $ par mois avec des serveurs illimités. Aucuns frais par application, par déploiement, par siège ou par gigaoctet.
- Des gabarits en un clic. Déployez des applications open source, comme des bases de données, des outils d'analyse et des outils de développement, sur votre propre serveur en un clic.
- Des bases de données gérées. Postgres, MySQL et Redis, avec des sauvegardes planifiées, vérifiées par restauration, et la récupération à un point dans le temps.
- Déploiement par push. Déployez depuis GitHub, GitLab ou la CLI, avec HTTPS automatique et des mises en production sans interruption.