Credit : Logo officiel
Installer WordPress sur un serveur Debian avec Nginx
Le cas typique : un VPS vide, un nom de domaine, et un client pressé
Samedi matin, un client commande un VPS Debian 12 chez IONOS et veut son site WordPress en ligne pour lundi. Cette stack Nginx + PHP 8.3 + MariaDB est ce que je déploie pour 80 % des projets WordPress en 2026 : plus rapide qu'Apache + mod_php, plus simple à tuner, et taillée pour 50 000 visiteurs/jour sur un VPS à 5 €/mois bien configuré. J'ai vu trop de tickets où le client a suivi un tuto de 2018 qui combine Apache, suEXEC, mod_security mal configuré, et qui se retrouve avec un site qui timeout à la 10ème connexion. Voici la procédure complète, les chemins exacts, les commandes que je tape sans réfléchir, et les pièges qui font perdre des heures.
Prérequis vérifiés en 2 minutes
# Connecté en SSH sur le VPS, vérifie l'OS
cat /etc/debian_version
# 12.5
# Confirme que tu es root ou que sudo marche
whoami
sudo -v
# Que le DNS pointe bien sur cette IP
dig +short monsite.fr
# Doit renvoyer l'IP publique du VPS
# Vérifie qu'aucun web ne tourne déjà
ss -tlnp | grep -E ':80|:443'
Si du apache2 traîne sur 80, désactive-le : systemctl disable --now apache2.
1. Mise à jour système et bonnes bases
apt update && apt upgrade -y
apt install -y curl wget gnupg2 ca-certificates lsb-release \
software-properties-common ufw fail2ban
Firewall minimal :
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
2. Installer Nginx
Le paquet Debian de Nginx (1.22) est correct, mais je préfère le repo officiel pour la 1.27 stable et HTTP/3 :
curl -fsSL https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
-o /usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/debian $(lsb_release -cs) nginx" \
> /etc/apt/sources.list.d/nginx.list
apt update && apt install nginx -y
systemctl enable --now nginx
# Tester
curl -I http://localhost
# HTTP/1.1 200 OK
# Server: nginx/1.27.x
3. PHP 8.3 et extensions
Debian 12 ship PHP 8.2. Pour PHP 8.3 ou 8.4, j'ajoute le repo Sury :
curl -sSL https://packages.sury.org/php/README.txt | bash -x
apt update
apt install -y php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd \
php8.3-intl php8.3-mbstring php8.3-soap php8.3-xml \
php8.3-zip php8.3-imagick php8.3-bcmath php8.3-opcache
php -v
# PHP 8.3.x (cli) (built: ...)
systemctl enable --now php8.3-fpm
Tuner PHP-FPM pour un VPS 2 Go RAM
; /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 16
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
; /etc/php/8.3/fpm/php.ini
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 60
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 60
systemctl restart php8.3-fpm
4. MariaDB et création de la base
apt install mariadb-server -y
systemctl enable --now mariadb
# Sécuriser
mysql_secure_installation
# Set root password? n (l'auth socket suffit pour root local)
# Remove anonymous? Y
# Disallow remote root? Y
# Remove test database? Y
# Reload privileges? Y
Créer la base WordPress
-- Connecté en root via sudo mysql
CREATE DATABASE wp_monsite CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'C9!xKp2vL@8nQz7r';
GRANT ALL PRIVILEGES ON wp_monsite.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Génère un mot de passe robuste avec openssl rand -base64 24.
Vérifier la connexion utilisateur
mysql -u wp_user -p wp_monsite -e "SHOW TABLES;"
# Vide à ce stade, c'est normal
Tuner MariaDB 10.11 pour un site WordPress
La config par défaut Debian est conservative. Pour un site WP avec 100 articles, j'ajoute :
; /etc/mysql/mariadb.conf.d/60-wordpress.cnf
[mysqld]
innodb_buffer_pool_size = 512M
innodb_log_file_size = 128M
innodb_flush_log_at_trx_commit = 2
query_cache_type = 0
query_cache_size = 0
tmp_table_size = 64M
max_heap_table_size = 64M
Le query_cache désactivé est un piège classique : sur MariaDB 10.6+, il dégrade les perfs. On le coupe au profit du buffer pool. Restart : systemctl restart mariadb.
5. Télécharger WordPress
cd /var/www
wget https://fr.wordpress.org/latest-fr_FR.tar.gz
tar -xzf latest-fr_FR.tar.gz
mv wordpress monsite.fr
rm latest-fr_FR.tar.gz
chown -R www-data:www-data /var/www/monsite.fr
find /var/www/monsite.fr -type d -exec chmod 755 {} \;
find /var/www/monsite.fr -type f -exec chmod 644 {} \;
6. Configurer le vhost Nginx
# /etc/nginx/conf.d/monsite.fr.conf
server {
listen 80;
listen [::]:80;
server_name monsite.fr www.monsite.fr;
root /var/www/monsite.fr;
index index.php index.html;
client_max_body_size 64M;
access_log /var/log/nginx/monsite.fr.access.log;
error_log /var/log/nginx/monsite.fr.error.log;
# Permaliens WordPress
location / {
try_files $uri $uri/ /index.php?$args;
}
# PHP
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_read_timeout 60s;
}
# Cache statique
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2|webp)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
# Sécurité
location ~ /\.(ht|git|env) { deny all; }
location = /xmlrpc.php { deny all; }
location ~* /(?:uploads|files)/.*\.php$ { deny all; }
# Headers sécurité
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
Tester et recharger :
nginx -t
# nginx: configuration file /etc/nginx/nginx.conf test is successful
systemctl reload nginx
7. Configurer wp-config.php
cd /var/www/monsite.fr
cp wp-config-sample.php wp-config.php
Édite wp-config.php :
define('DB_NAME', 'wp_monsite');
define('DB_USER', 'wp_user');
define('DB_PASSWORD', 'C9!xKp2vL@8nQz7r');
define('DB_HOST', 'localhost');
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', 'utf8mb4_unicode_520_ci');
// Salts uniques (récupère sur https://api.wordpress.org/secret-key/1.1/salt/)
define('AUTH_KEY', '...');
define('SECURE_AUTH_KEY', '...');
// etc.
$table_prefix = 'wp_xq_'; // change le préfixe par défaut
define('WP_DEBUG', false);
define('DISALLOW_FILE_EDIT', true);
define('FS_METHOD', 'direct');
define('WP_AUTO_UPDATE_CORE', 'minor');
define('WP_MEMORY_LIMIT', '256M');
Génère les salts en CLI :
curl -s https://api.wordpress.org/secret-key/1.1/salt/
Colle le bloc dans wp-config.php.
8. SSL avec Certbot
apt install certbot python3-certbot-nginx -y
certbot --nginx -d monsite.fr -d www.monsite.fr \
--redirect --hsts --staple-ocsp \
-m admin@monsite.fr --agree-tos --no-eff-email
Certbot ajoute automatiquement les listen 443 ssl http2, les directives SSL et la redirection HTTP→HTTPS.
Vérifier le renouvellement automatique :
systemctl list-timers | grep certbot
certbot renew --dry-run
9. Installer WordPress via WP-CLI (alternative à l'assistant web)
Pour scripter l'install (utile pour des dizaines de sites) :
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp
cd /var/www/monsite.fr
sudo -u www-data wp core install \
--url="https://monsite.fr" \
--title="Mon Site" \
--admin_user="dylan" \
--admin_password="$(openssl rand -base64 16)" \
--admin_email="admin@monsite.fr"
WordPress est installé, table wp_xq_users créée, admin opérationnel. Note le mot de passe affiché.
10. Hardening post-install
# Plugin de sécurité minimum
sudo -u www-data wp plugin install wordfence wp-super-cache --activate
# Désactiver les éditions de thèmes/plugins en backoffice
sudo -u www-data wp config set DISALLOW_FILE_MODS true --raw
# Bloquer les commentaires si pas de blog
sudo -u www-data wp option update default_comment_status closed
sudo -u www-data wp option update default_ping_status closed
Fail2ban avec un jail WordPress :
# /etc/fail2ban/jail.d/wordpress.conf
[wordpress-auth]
enabled = true
filter = wordpress-auth
logpath = /var/log/nginx/monsite.fr.access.log
maxretry = 5
bantime = 3600
port = http,https
11. Cache objets avec Redis
WordPress refait des dizaines de requêtes SQL par page. Redis comme cache d'objets divise ce nombre par 5 sans modifier le code :
apt install redis-server php8.3-redis -y
systemctl enable --now redis-server
cd /var/www/monsite.fr
sudo -u www-data wp plugin install redis-cache --activate
sudo -u www-data wp redis enable
Dans wp-config.php, ajoute le sel pour isoler les caches multi-sites :
define('WP_REDIS_PREFIX', 'monsite_');
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);
Vérifie le statut : wp redis status doit afficher Status: Connected. Sur un VPS bien dimensionné, le TTFB passe de 600 ms à 80 ms sur les pages dynamiques.
12. Vérifier la performance
# TTFB depuis le serveur
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
https://monsite.fr
# Charge sur 50 requêtes parallèles
apt install apache2-utils
ab -n 500 -c 50 https://monsite.fr/
# Requests per second: 200+ visé sur VPS 2 Go
Si le TTFB dépasse 500 ms, vérifie OPcache, ajoute Redis pour le cache objets WordPress.
Pourquoi cette stack plutôt qu'une autre
Quand un client me demande pourquoi je ne déploie pas avec Docker ou avec un PaaS, ma réponse est pragmatique. Docker ajoute une couche de complexité (réseaux, volumes, restart policies) qui ne se justifie que si tu as plusieurs sites à isoler proprement, ou un pipeline CI/CD multi-environnements. Pour un site WordPress unique sur un VPS, l'install native est plus rapide à débugger, plus facile à monitorer avec les outils standards Debian, et ne consomme pas les 200 Mo supplémentaires du runtime Docker.
Les PaaS type Vercel ou Netlify ne servent pas WordPress (pas de PHP runtime persistent), et les solutions WP-as-a-service comme WP Engine partent à 25 €/mois pour un seul site. Sur un VPS bien configuré, j'ai 5 sites WP qui tournent confortablement à 5 €/mois total, soit 1 €/site. La courbe d'apprentissage de la stack Linux est rentabilisée dès le premier site.
Quand je passe sur Apache à la place
Deux cas où je remplace Nginx par Apache : un client qui a des règles .htaccess complexes héritées d'un autre hébergement (réécrire toutes ces règles en Nginx prend des heures), et un site PrestaShop ou Magento qui charge des modules avec leurs propres .htaccess. Pour WordPress pur, Nginx reste vraiment plus rapide et plus prévisible sous charge soutenue.
13. Monitorer en continu
Dernier étage de la stack : la surveillance. Sans monitoring, tu apprends que ton site est down par un mail client. Mon kit minimum sur le même VPS :
apt install netdata -y
# Dashboard live sur :19999, à protéger derrière auth Nginx
Netdata expose CPU, RAM, disque, réseau, MySQL, PHP-FPM, Nginx en temps réel. En complément, un check externe gratuit via UptimeRobot avec alerte SMS si le site renvoie 5xx pendant plus de 2 minutes. Coût total : 0 €. Valeur quand un disque commence à saturer à 3h du matin : inestimable.
Sauvegardes : la dernière chose à oublier
Un site WordPress sans sauvegarde, c'est un site qui finira par disparaître. Je rajoute systématiquement après l'install un script backup.sh qui dump la base, archive /var/www/monsite.fr et synchronise vers un stockage objet externe :
#!/bin/bash
# /usr/local/bin/backup-wp.sh
DATE=$(date +%Y%m%d-%H%M)
DEST=/var/backups/wp
mkdir -p $DEST
# Dump base
mysqldump --single-transaction wp_monsite | \
gzip > $DEST/db-$DATE.sql.gz
# Archive fichiers (sans le cache)
tar --exclude='wp-content/cache' --exclude='wp-content/uploads/cache' \
-czf $DEST/files-$DATE.tar.gz /var/www/monsite.fr
# Push vers Backblaze B2
rclone copy $DEST/ b2:backup-wp/$DATE/ --include "*-$DATE.*"
# Rotation 30 jours
find $DEST -mtime +30 -delete
Cron : 30 3 * * * /usr/local/bin/backup-wp.sh. Le coût Backblaze pour 5 Go de sauvegardes WordPress versionnées : environ 0,03 € par mois. Je n'ai jamais rencontré un client qui regrette d'avoir investi ces 30 minutes de setup quand un plugin a corrompu la base ou un hack a effacé wp-content. Teste la restauration au moins une fois par trimestre sur un environnement isolé : une sauvegarde qu'on n'a jamais restaurée n'est qu'une promesse de sauvegarde.
Erreurs courantes et leur fix
502 Bad Gateway à la première visite
Cause : Nginx pointe sur le mauvais socket PHP-FPM. Vérifie :
ls -la /run/php/
# Cherche php8.3-fpm.sock — si c'est php8.2-fpm.sock, ajuste le vhost Nginx.
Error establishing a database connection
Cause : creds DB incorrects ou MariaDB pas démarré. Test :
systemctl status mariadb
mysql -u wp_user -p -e "USE wp_monsite; SHOW TABLES;"
The link you followed has expired à l'upload de média
Cause : upload_max_filesize ou post_max_size PHP trop bas. Édite /etc/php/8.3/fpm/php.ini, monte à 64M, restart php8.3-fpm.
Permaliens 404 après changement de structure
Cause : try_files Nginx mal configuré. Confirme le bloc location / du vhost contient bien try_files $uri $uri/ /index.php?$args;.
Boucle de redirection HTTPS infinie après Cloudflare
Cause : Cloudflare en mode "Flexible SSL" parle HTTP au backend, WP renvoie HTTPS, Cloudflare redirige... Solution : passe Cloudflare en mode "Full (strict)" et ajoute dans wp-config.php :
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
White screen of death après un upgrade plugin
Cause : un plugin est incompatible avec PHP 8.3. Active WP_DEBUG :
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Puis lis /var/www/monsite.fr/wp-content/debug.log. En général tu vois Fatal error: Uncaught Error: Call to undefined function.... Désactive le plugin coupable via WP-CLI : wp plugin deactivate nom-plugin --skip-plugins.
Pour aller plus loin
- Commandes WP-CLI essentielles — gérer WordPress en CLI
- Optimiser les performances WordPress — cache, CDN, image optim
- Sécuriser WordPress contre les hackers — guide complet
- Configurer Redis comme cache WordPress — booster les pages dynamiques
- Déboguer WordPress qui ne charge plus — quand ça casse en prod
- Activer le mode debug WordPress — la première étape pour diagnostiquer
Ce qui change tout en prod
En 2026, mes WordPress tournent tous avec PHP 8.3, OPcache activé et Redis pour les objets cachés. Sur un VPS S à 1 €/mois, ça encaisse 30k visiteurs/mois sans broncher. Le secret n'est pas dans WordPress lui-même mais dans la stack qui l'entoure : Nginx bien tuné, PHP-FPM dimensionné, base UTF8MB4, certificats auto-renouvelés. Une fois cette base posée, tu n'y reviens plus pendant des mois. Le piège classique reste de bourrer le site de plugins "d'optimisation" qui se contredisent — mieux vaut une stack serveur propre qu'un WP plein de cache plugins concurrents. Garde la liste de plugins courte (10 max), tiens la stack à jour avec apt upgrade tous les 15 jours, et programme tes mises à jour core/plugins à un horaire de faible trafic via WP-CLI plutôt qu'en clic dans l'admin.