Serveur : migration vers Ubuntu 26.04 LTS depuis Ubuntu 24.04 LTS

Ubuntu 26.04 LTS, nom de code Resolute Raccoon, est disponible depuis le 23 avril 2026. C’est une version LTS, donc une version de support longue durée, maintenue jusqu’en avril 2031 en support standard, avec une extension possible via Ubuntu Pro.

Comme toujours avec une LTS, la promesse est simple : moins de panique, plus de stabilité. Enfin, en théorie. En pratique, une migration de serveur reste une opération chirurgicale : il faut préparer, sauvegarder, lire les logs, tester les services, puis corriger les petits dragons qui sortent du bois.

Dans mon cas, la migration d’un serveur Ubuntu 24.04 LTS vers Ubuntu 26.04 LTS s’est globalement bien passée. Cependant, trois services ont demandé une intervention après redémarrage :

  • Nginx, car le support Brotli nécessite désormais d’installer explicitement les modules Brotli.
  • Dovecot, car la configuration 2.3 ne fonctionne plus telle quelle avec Dovecot 2.4.
  • Postfix, car certaines directives TLS historiques sont désormais obsolètes ou remplacées.

Rien d’insurmontable, mais assez pour transformer une mise à jour tranquille en soirée « journalctl et bière triple ».

Kinsta: Premium Managed WordPress hosting

Ce qu’apporte Ubuntu 26.04 LTS

Ubuntu 26.04 LTS consolide deux années d’évolutions depuis Ubuntu 24.04 LTS. Si vous migrez depuis 24.04, vous récupérez donc les changements introduits dans les versions intermédiaires 24.10, 25.04 et 25.10, puis ceux propres à 26.04.

Canonical met surtout l’accent sur la sécurité, la résilience, le support matériel récent, les composants mémoire-safe, les permissions applicatives, le chiffrement TPM, les optimisations serveur et le support Arm Livepatch.

Pour un serveur, les points intéressants sont les suivants.

Support long terme jusqu’en 2031

Ubuntu 26.04 LTS reçoit cinq ans de correctifs de sécurité et de maintenance critique. Cela en fait une base saine pour un serveur web, un serveur mail, une stack WordPress/WooCommerce, un serveur Nextcloud ou une machine dédiée OVH.

Avec Ubuntu Pro, le support étendu peut aller jusqu’à dix ans. C’est utile pour les environnements où l’on ne veut pas refaire une migration complète tous les cinq ans.

Kernel plus récent et meilleur support matériel

Ubuntu 26.04 LTS arrive avec un noyau plus moderne, ce qui améliore le support du matériel récent : CPU, contrôleurs réseau, stockage NVMe, cartes graphiques, pilotes et plateformes serveur plus récentes. Pour un serveur dédié, ce n’est pas forcément spectaculaire au quotidien, mais cela compte dès qu’on utilise du matériel récent ou des fonctionnalités bas niveau.

Sur une machine OVH, Hetzner, Scaleway ou un serveur maison, cela peut régler des détails pénibles : meilleure gestion d’énergie, meilleure détection matériel, pilotes réseau plus propres, ou meilleure prise en charge du stockage.

Sécurité renforcée

Ubuntu 26.04 LTS continue le gros travail de durcissement côté système. Canonical cite notamment le chiffrement complet du disque adossé au TPM, une meilleure gestion des permissions applicatives, plus de composants mémoire-safe, ainsi que Livepatch sur davantage de plateformes.

Sur un serveur web, ce n’est pas une raison pour relâcher AppArmor, SSH, UFW/nftables, Fail2ban ou les mises à jour automatiques. Mais c’est une base plus moderne.

Toolchain et paquets serveur plus récents

Comme toujours, une nouvelle LTS apporte des versions plus récentes des outils système, bibliothèques, compilateurs, runtimes et paquets serveur.

C’est une bonne nouvelle pour les stacks modernes, mais cela peut casser des configurations anciennes. Et c’est précisément ce qui m’est arrivé avec Dovecot et Postfix. Les fichiers de configuration qui vivaient leur meilleure vie depuis dix ans n’aiment pas toujours le futur.

Faut-il migrer tout de suite ?

Sur un poste de test, oui. Sur un serveur de production, cela dépend.

Canonical indique que les utilisateurs d’Ubuntu 25.10 se verront proposer la mise à niveau vers 26.04 rapidement, tandis que les utilisateurs d’Ubuntu 24.04 LTS recevront normalement la proposition de mise à niveau automatique avec Ubuntu 26.04.1 LTS, prévue pour le 4 août 2026.

Donc, pour un serveur client critique, j’aurais tendance à attendre 26.04.1, sauf besoin réel. Pour un serveur personnel, un serveur bien sauvegardé, ou une machine que l’on sait réparer en SSH, la migration est faisable maintenant.

Dans tous les cas : sauvegarde d’abord, héroïsme ensuite.

Préparer la migration

Avant de lancer la migration, je commence par faire le point.

lsb_release -a
uname -a
uptime
df -h
free -h

Ensuite, je vérifie les services critiques :

systemctl --failed
systemctl status nginx --no-pager
systemctl status php8.3-fpm --no-pager
systemctl status mysql --no-pager
systemctl status postfix --no-pager
systemctl status dovecot --no-pagerLangage du code : CSS (css)

Adaptez évidemment les services à votre stack.

Sur un serveur WordPress classique, je surveille au minimum :

  • Nginx ou Apache
  • PHP-FPM
  • MySQL ou MariaDB
  • Redis
  • Postfix
  • Dovecot
  • Fail2ban
  • Cron
  • Certbot ou acme.sh
  • UFW/nftables
  • CrowdSec, si installé
  • Services maison dans /etc/systemd/system/

Sauvegarder avant de migrer

Une migration LTS sans sauvegarde, c’est comme éditer functions.php depuis l’éditeur WordPress. Techniquement possible. Spirituellement douteux.

Je crée d’abord un dossier de sauvegarde daté :

sudo mkdir -p /root/backup-before-ubuntu-26.04
cd /root/backup-before-ubuntu-26.04

Puis je sauvegarde les configurations principales :

sudo tar -czf etc-backup-$(date +%F-%H%M).tar.gz /etcLangage du code : JavaScript (javascript)

Je sauvegarde ensuite la liste des paquets installés :

dpkg --get-selections > packages-$(date +%F-%H%M).txt
apt-mark showmanual > manual-packages-$(date +%F-%H%M).txtLangage du code : JavaScript (javascript)

Puis les dépôts APT :

sudo tar -czf apt-sources-$(date +%F-%H%M).tar.gz \
  /etc/apt/sources.list \
  /etc/apt/sources.list.d \
  /etc/apt/keyrings 2>/dev/nullLangage du code : JavaScript (javascript)

Sur un serveur web, je sauvegarde aussi les vhosts :

sudo tar -czf nginx-backup-$(date +%F-%H%M).tar.gz /etc/nginx
sudo tar -czf php-backup-$(date +%F-%H%M).tar.gz /etc/phpLangage du code : JavaScript (javascript)

Pour le serveur mail :

sudo tar -czf mail-backup-$(date +%F-%H%M).tar.gz \
  /etc/postfix \
  /etc/dovecot \
  /etc/opendkim* \
  /etc/mailname 2>/dev/nullLangage du code : JavaScript (javascript)

Et si MySQL tourne sur la machine :

sudo mysqldump --all-databases --single-transaction --quick --routines --events \
  > mysql-all-databases-$(date +%F-%H%M).sqlLangage du code : JavaScript (javascript)

Ensuite, je copie tout cela hors du serveur.

rsync -avz /root/backup-before-ubuntu-26.04/ user@backup-server:/backups/apollo/Langage du code : JavaScript (javascript)

Remplacez user@backup-server par votre vraie destination.

Vérifier l’espace disque

Une migration de distribution a besoin d’espace. Si /boot, /, /var ou /usr sont trop pleins, vous allez passer un moment désagréable.

df -h
sudo du -xhd1 /var | sort -h
sudo du -xhd1 /usr | sort -hLangage du code : JavaScript (javascript)

Nettoyez les anciens paquets :

sudo apt autoremove --purge
sudo apt clean

Puis vérifiez les kernels installés :

dpkg -l 'linux-image*' | awk '/^ii/{print $2}'Langage du code : JavaScript (javascript)

Si /boot est plein, ne lancez pas la migration avant d’avoir nettoyé proprement les anciens noyaux.

Mettre Ubuntu 24.04 à jour avant la migration

Avant de changer de version, on met la version actuelle parfaitement à jour.

sudo apt update
sudo apt full-upgrade
sudo apt autoremove --purge
sudo reboot

Après redémarrage :

sudo apt update
sudo apt full-upgrade
systemctl --failed

Si des paquets sont cassés ou retenus, corrigez avant de migrer.

sudo apt --fix-broken install
sudo dpkg --configure -a
apt-mark showhold

Vérifier les dépôts tiers

C’est souvent là que les migrations se passent mal.

Listez les dépôts tiers :

find /etc/apt/sources.list.d -type f -maxdepth 1 -print -exec cat {} \;Langage du code : PHP (php)

Si vous avez des dépôts externes pour PHP, Nginx, MySQL, MariaDB, Redis, Node.js, Docker ou autre, notez-les.

Pendant la migration, Ubuntu désactive souvent les dépôts tiers. C’est normal. En revanche, après migration, il faudra réactiver uniquement ceux qui sont compatibles avec Ubuntu 26.04.

Sur un serveur de production, je préfère réduire au maximum les dépôts exotiques avant une LTS. Moins de magie, moins de flammes. J’ai par exemple supprimé les dépôts Sury car il a laissé tomber les mises à jour NginX donc autant rester sur les dépôts Ubuntu officiels.

Kinsta: Premium Managed WordPress hosting

Lancer la migration vers Ubuntu 26.04 LTS en SSH

Canonical recommande do-release-upgrade pour les serveurs et les images cloud, car l’outil gère les changements de configuration nécessaires entre versions. (Ubuntu)

Installez l’outil si besoin :

sudo apt install update-manager-core

Vérifiez que le canal LTS est activé :

grep Prompt /etc/update-manager/release-upgrades

Vous devez obtenir :

Prompt=lts

Si ce n’est pas le cas :

sudo nano /etc/update-manager/release-upgrades

Puis réglez :

Prompt=lts

Utiliser screen ou tmux

Ne lancez jamais une migration distante dans une session SSH nue.

Installez screen :

sudo apt install screen

Puis lancez une session :

screen -S ubuntu-upgrade

Si votre connexion SSH tombe, reconnectez-vous puis récupérez la session :

screen -r ubuntu-upgrade

Démarrer la mise à niveau

La commande standard est :

sudo do-release-upgradeLangage du code : JavaScript (javascript)

Cependant, juste après la sortie de 26.04, la mise à niveau depuis 24.04 LTS peut ne pas être proposée automatiquement avant 26.04.1. Canonical indique que la proposition automatique depuis 24.04 LTS arrive avec 26.04.1 LTS.

Si vous êtes en production, le choix raisonnable est donc d’attendre.

Si vous acceptez d’être plus offensif, vous pouvez tester :

sudo do-release-upgrade -dLangage du code : JavaScript (javascript)

Attention : -d demande à l’outil de regarder les versions de développement ou les mises à niveau non encore proposées par le canal standard. Canonical déconseille ce drapeau pour les environnements de production.

Sur un serveur personnel bien sauvegardé, cela peut se défendre. Sur un serveur client, non.

Pendant la migration

L’outil va poser plusieurs questions.

En général, je garde les fichiers de configuration existants quand ils ont été personnalisés, puis je compare ensuite avec les versions mainteneur.

Quand do-release-upgrade propose :

install the package maintainer's version?
keep the local version currently installed?
show the differences?

Je choisis toujours :

D

pour voir le diff.

Puis, selon le fichier :

  • Je garde ma version pour les vhosts Nginx.
  • Je garde ma version pour Postfix/Dovecot si elle est très custom.
  • J’accepte la version mainteneur pour des fichiers peu modifiés.
  • Je note tous les fichiers .dpkg-dist, .dpkg-old et .ucf-dist.

Après la migration, ces fichiers sont précieux :

sudo find /etc -type f \( -name "*.dpkg-dist" -o -name "*.dpkg-old" -o -name "*.ucf-dist" \) -printLangage du code : PHP (php)

Redémarrer

À la fin :

sudo reboot

Puis reconnectez-vous.

Vérifications après redémarrage

On commence par vérifier la version :

lsb_release -a
cat /etc/os-release
uname -a

Puis les services cassés :

systemctl --failed

Ensuite, les logs du boot courant :

journalctl -p err -b --no-pager

Puis les services essentiels :

systemctl status nginx --no-pager
systemctl status postfix --no-pager
systemctl status dovecot --no-pager
systemctl status php8.3-fpm --no-pager
systemctl status mysql --no-pagerLangage du code : CSS (css)

Sur mon serveur, les trois sujets intéressants ont été Nginx, Dovecot et Postfix.

Problème 1 : Nginx et Brotli

Après migration, Nginx peut refuser de démarrer avec une erreur du genre :

unknown directive "brotli"Langage du code : JavaScript (javascript)

Cela arrive lorsque votre configuration contient des directives Brotli :

brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;

Mais que les modules Brotli ne sont pas installés ou plus chargés.

Diagnostiquer

Testez la configuration :

sudo nginx -t

Puis regardez le statut :

sudo systemctl status nginx --no-pager

Et les logs :

sudo journalctl -u nginx -b --no-pager

Cherchez les directives Brotli :

grep -R "brotli" /etc/nginx -nLangage du code : JavaScript (javascript)

Vérifiez les modules Nginx disponibles :

find /usr/lib/nginx/modules -maxdepth 1 -type f -name '*.so' | sortLangage du code : JavaScript (javascript)

Puis les modules chargés :

grep -R "^[[:space:]]*load_module" /etc/nginx /usr/share/nginx 2>/dev/nullLangage du code : JavaScript (javascript)

Installer le module Brotli

Sur Ubuntu 26.04, cherchez le paquet disponible :

apt-cache search brotli | grep -i nginx

Les paquets disponibles sont :

libnginx-mod-http-brotli-filter - Brotli lossless compression support for Nginx - filter
libnginx-mod-http-brotli-static - Brotli lossless compression support for Nginx - staticLangage du code : JavaScript (javascript)

Installez-les :

sudo apt update
sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-staticLangage du code : JavaScript (javascript)

Puis testez :

sudo nginx -t

Si tout est bon :

sudo systemctl restart nginx

Et vérifiez :

systemctl status nginx --no-pager

Tester Brotli côté HTTP

Depuis votre machine locale :

curl -sI -H 'Accept-Encoding: br' https://example.com/ | grep -i 'content-encoding'Langage du code : JavaScript (javascript)

Si Brotli est actif sur la ressource demandée, vous devriez voir :

content-encoding: brLangage du code : HTTP (http)

Attention : si Cloudflare est devant le site, il peut aussi gérer Brotli. Donc testez aussi directement l’origine si besoin.

Kinsta: Premium Managed WordPress hosting

Problème 2 : Dovecot et les directives renommées

Dovecot 2.4 introduit de vrais changements de configuration. Ce n’est pas une mini-dépréciation cosmétique.

La documentation officielle indique clairement que les configurations Dovecot 2.3 ne fonctionnent pas sans adaptation. Elle précise aussi que le premier réglage de dovecot.conf doit désormais être dovecot_config_version, et qu’un autre réglage obligatoire, dovecot_storage_version, doit être défini. (doc.dovecot.org)

En clair : si votre fichier de configuration a traversé plusieurs LTS, il peut casser au redémarrage.

Symptôme

Dovecot ne démarre plus :

systemctl status dovecot --no-pager

Ou :

journalctl -u dovecot -b --no-pager

Vous pouvez voir une erreur du genre :

Fatal: The first setting must be dovecot_config_versionLangage du code : HTTP (http)

Diagnostiquer proprement

Lancez :

sudo doveconf -n

Puis :

sudo dovecot -n

Ensuite, inspectez les fichiers actifs :

sudo grep -R "^[^#]" /etc/dovecot/ -nLangage du code : JavaScript (javascript)

Ajouter les directives obligatoires

Éditez :

sudo nano /etc/dovecot/dovecot.conf

Le premier réglage actif doit être :

dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0

Placez ces lignes tout en haut du fichier, avant les inclusions du type :

!include_try /usr/share/dovecot/protocols.d/*.protocol
!include conf.d/*.conf

Cela donne par exemple :

dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0

!include_try /usr/share/dovecot/protocols.d/*.protocol
!include conf.d/*.conf
!include_try local.conf

Puis testez :

sudo doveconf -n

Mais attention : cela ne suffit pas toujours.

Dovecot 2.4 a modifié plusieurs noms de directives, ainsi que la syntaxe des variables. La documentation officielle liste notamment le remplacement de variables comme %u, %d, %n, etc. Par exemple, %u devient %{user} et %d devient %{user | domain}. (doc.dovecot.org)

Corriger les certificats TLS

Dans Dovecot 2.3, on utilisait souvent :

ssl_cert = </etc/ssl/certs/dovecot.pem
ssl_key = </etc/ssl/private/dovecot.pemLangage du code : JavaScript (javascript)

En Dovecot 2.4, ces directives deviennent :

ssl_server_cert_file = /etc/ssl/certs/dovecot.pem
ssl_server_key_file = /etc/ssl/private/dovecot.pemLangage du code : JavaScript (javascript)

La documentation officielle liste bien ces renommages : ssl_cert devient ssl_server_cert_file, et ssl_key devient ssl_server_key_file. (doc.dovecot.org)

Notez aussi un changement important : avec les nouvelles directives, on donne le chemin du fichier. On ne met plus le préfixe < comme dans l’ancien style.

Donc, si vous aviez :

ssl = required
ssl_cert = </etc/nginx/ssl/skyminds.net/fullchain.pem
ssl_key = </etc/nginx/ssl/skyminds.net/privkey.pemLangage du code : JavaScript (javascript)

Passez à :

ssl = required
ssl_server_cert_file = /etc/nginx/ssl/skyminds.net/fullchain.pem
ssl_server_key_file = /etc/nginx/ssl/skyminds.net/privkey.pemLangage du code : JavaScript (javascript)

Puis :

sudo doveconf -n
sudo systemctl restart dovecot
sudo systemctl status dovecot --no-pager

Corriger mail_location

Ancienne syntaxe possible :

mail_location = maildir:~/Maildir

Nouvelle syntaxe :

mail_driver = maildir
mail_path = ~/Maildir

Pour un stockage mbox, on peut rencontrer un cas comme :

mail_location = mbox:~/mail:INBOX=/var/spool/mail/%uLangage du code : JavaScript (javascript)

Qui devient :

mail_driver = mbox
mail_path = ~/mail
mail_inbox_path = /var/spool/mail/%{user}Langage du code : PHP (php)

L’idée importante : Dovecot 2.4 sépare davantage les paramètres, et l’ancienne syntaxe compacte ne passe plus forcément.

Corriger les variables

Cherchez les anciennes variables :

sudo grep -R "%[udnrmls]" /etc/dovecot -nLangage du code : JavaScript (javascript)

Exemples fréquents :

%u
%d
%n
%r

Remplacements courants :

%u → %{user}
%d → %{user | domain}
%n → %{user | username}
%r → %{remote_ip}
%l → %{local_ip}
%s → %{protocol} ou %{service}, selon le contexte

Ne faites pas un sed global à l’aveugle. C’est tentant. C’est aussi comme remplacer toutes les virgules par des points-virgules dans un roman : parfois ça compile, souvent ça pique.

Tester IMAP et IMAPS

Une fois Dovecot redémarré :

sudo ss -ltnp | grep dovecot

Vous devriez voir les ports attendus :

143
993

Test TLS local :

openssl s_client -connect localhost:993 -servername mail.example.comLangage du code : CSS (css)

Puis vérifiez les logs :

journalctl -u dovecot -b --no-pager

Problème 3 : Postfix et les directives TLS obsolètes

Postfix est souvent plus tolérant que Dovecot. Dans mon cas, Postfix démarrait, mais affichait des avertissements sur des directives obsolètes.

C’est typique des fichiers main.cf qui ont vécu plusieurs générations de Debian/Ubuntu.

La documentation Postfix liste les fonctionnalités dépréciées et leurs remplaçants. Elle indique notamment que smtpd_use_tls est remplacé par smtpd_tls_security_level. (Postfix)

Diagnostiquer

Commencez par afficher la configuration active :

postconf -n

Puis cherchez les directives TLS anciennes :

postconf -n | grep -E 'smtpd_use_tls|smtpd_enforce_tls|smtp_use_tls|smtp_enforce_tls|smtpd_tls_cert_file|smtpd_tls_key_file'Langage du code : JavaScript (javascript)

Regardez ensuite les logs :

journalctl -u postfix -b --no-pager

Et le statut :

systemctl status postfix --no-pager

Remplacer smtpd_use_tls

Ancienne directive :

smtpd_use_tls = yes

Nouvelle directive :

smtpd_tls_security_level = may

Appliquez avec postconf :

sudo postconf -e 'smtpd_tls_security_level = may'
sudo postconf -X smtpd_use_tlsLangage du code : JavaScript (javascript)

Si vous aviez :

smtpd_enforce_tls = yes

Cela correspond plutôt à :

smtpd_tls_security_level = encrypt

Mais attention : encrypt force TLS. Sur un serveur MX public, on utilise généralement may pour le SMTP entrant, car le courrier doit pouvoir arriver même si le serveur distant ne sait pas négocier TLS.

Pour un service Submission sur le port 587, en revanche, on configure souvent les exigences TLS dans master.cf.

Moderniser les certificats TLS Postfix

Postfix supporte encore :

smtpd_tls_cert_file = /etc/nginx/ssl/example.com/fullchain.pem
smtpd_tls_key_file = /etc/nginx/ssl/example.com/privkey.pemLangage du code : JavaScript (javascript)

Mais depuis Postfix 3.4, la méthode recommandée pour clés et certificats serveur est smtpd_tls_chain_files. La documentation TLS de Postfix indique que cette interface est préférable, notamment pour éviter les soucis de cohérence entre clé et certificat. (Postfix)

Avec smtpd_tls_chain_files, l’ordre compte : clé privée d’abord, puis chaîne de certificats.

Exemple :

smtpd_tls_chain_files =
    /etc/nginx/ssl/skyminds.net/privkey.pem,
    /etc/nginx/ssl/skyminds.net/fullchain.pemLangage du code : JavaScript (javascript)

Avec postconf :

sudo postconf -e 'smtpd_tls_chain_files = /etc/nginx/ssl/skyminds.net/privkey.pem, /etc/nginx/ssl/skyminds.net/fullchain.pem'Langage du code : JavaScript (javascript)

Puis vous pouvez retirer les anciennes directives si vous basculez vraiment :

sudo postconf -X smtpd_tls_cert_file
sudo postconf -X smtpd_tls_key_file

Redémarrez :

sudo systemctl restart postfix
sudo systemctl status postfix --no-pager

Vérifier Submission et SMTPS

Ports habituels :

sudo ss -ltnp | grep master

Test STARTTLS sur 587 :

openssl s_client -starttls smtp -connect localhost:587 -servername mail.example.comLangage du code : CSS (css)

Test SMTPS sur 465 :

openssl s_client -connect localhost:465 -servername mail.example.comLangage du code : CSS (css)

Puis logs :

journalctl -u postfix -b --no-pager

Et configuration finale :

postconf -n
Kinsta: Premium Managed WordPress hosting

Problème 4 : autres problèmes Postfix

Postfix : l’IP du serveur n’est pas détectée au démarrage

Après la migration vers Ubuntu 26.04 LTS, Postfix refusait de démarrer avec cette erreur :

fatal: parameter inet_interfaces: no local interface found for 152.XXX.XXX.XXXLangage du code : CSS (css)

Le service était donc en échec :

systemctl --failed
systemctl status postfix --no-pager -l

Dans mon cas, l’IP existait bien une fois le serveur démarré :

ip -br addr
hostname -I

Résultat :

eno1 UP 152.XXX.XXX.XXX/24
152.XXX.XXX.XXX 2001:XXXX:XXX:XXXX::

La configuration Postfix était trop stricte :

inet_interfaces = 127.0.0.1, 152.XXX.XXX.XXX

Le problème venait probablement de l’ordre de démarrage : Postfix se lançait avant que l’interface réseau ait fini de recevoir son adresse IP. Au moment précis où Postfix testait 152.XXX.XXX.XXX, l’adresse n’était pas encore bindée sur eno1. Quelques secondes plus tard, elle existait bien. Merci systemd, ce petit farceur.

La solution la plus robuste consiste à laisser Postfix écouter sur toutes les interfaces disponibles :

postconf -e 'inet_interfaces = all'
postfix check
systemctl restart postfix
systemctl status postfix --no-pager -lLangage du code : JavaScript (javascript)

On vérifie ensuite :

postconf -n | grep '^inet_interfaces'
ss -ltnp | grep masterLangage du code : JavaScript (javascript)

Postfix doit alors écouter sur les ports attendus, notamment 25 et 587.

Si vous tenez absolument à binder Postfix sur une IP précise, il faut plutôt forcer le service à attendre que le réseau soit vraiment en ligne :

systemctl edit postfix

Puis ajouter :

[Unit]
Wants=network-online.target
After=network-online.target

Ensuite :

systemctl daemon-reload
systemctl restart postfix

Sur mon serveur, j’ai préféré inet_interfaces = all. C’est plus simple, plus robuste, et le pare-feu reste le vrai garde-fou.

Postfix : ancien cron avec postfix@-.service

Après avoir corrigé Postfix, j’ai reçu un email cron assez cryptique :

Cron <root@apollo> /usr/bin/systemctl restart postfix@-.service
Assertion failed on job for postfix@-.service.Langage du code : HTML, XML (xml)

Ce cron venait d’une ancienne habitude : redémarrer Postfix tous les jours pour éviter qu’un mail ne reste bloqué. La commande utilisée était :

/usr/bin/systemctl restart postfix@-.service # daily reboot for postfix services, to ensure no mail is missedLangage du code : PHP (php)

Sur Ubuntu 26.04 LTS, l’unité active utilisée par Postfix est simplement :

postfix.serviceLangage du code : CSS (css)

On peut le vérifier avec :

systemctl status postfix --no-pager -l
systemctl list-unit-files 'postfix*'
systemctl list-units 'postfix*' --allLangage du code : PHP (php)

La correction minimale consiste donc à remplacer l’ancienne commande par :

/usr/bin/systemctl restart postfix.service

Ou plus simplement :

/usr/bin/systemctl restart postfix

Mais, franchement, si Postfix fonctionne correctement, ce redémarrage quotidien n’est pas nécessaire. Postfix sait gérer sa queue tout seul. S’il ne peut pas envoyer un message immédiatement, il réessaie selon sa politique de retry.

On peut donc soit supprimer ce cron, soit le remplacer par une vérification beaucoup plus propre :

15 5 * * * /usr/sbin/postfix checkLangage du code : JavaScript (javascript)

Pour retrouver l’ancien cron :

crontab -l | grep -i postfix
grep -R "postfix@-\|restart postfix" /etc/cron* /var/spool/cron/crontabs -n 2>/dev/nullLangage du code : JavaScript (javascript)

Après modification :

systemctl restart postfix.service
systemctl reset-failed postfix.service
systemctl --failedLangage du code : CSS (css)

Postfix : warning sur /var/spool/postfix/etc/resolv.conf

Une fois Postfix relancé, postfix check affichait encore ce warning :

postfix/postfix-script: warning: not owned by root: /var/spool/postfix/etc/resolv.confLangage du code : JavaScript (javascript)

Le fichier existait bien, mais il appartenait à systemd-resolve :

ls -l /var/spool/postfix/etc/resolv.conf
stat /var/spool/postfix/etc/resolv.confLangage du code : JavaScript (javascript)

Résultat :

-rw-r--r-- 1 systemd-resolve systemd-resolve 920 Apr 24 00:50 /var/spool/postfix/etc/resolv.confLangage du code : JavaScript (javascript)

Comme ce fichier se trouve dans le chroot Postfix, Postfix préfère qu’il appartienne à root.

Correction :

chown root:root /var/spool/postfix/etc/resolv.conf
chmod 0644 /var/spool/postfix/etc/resolv.conf
postfix check
systemctl restart postfixLangage du code : JavaScript (javascript)

Si le propriétaire revient à systemd-resolve après un reboot, on peut forcer proprement les permissions avec systemd-tmpfiles :

cat >/etc/tmpfiles.d/postfix-chroot-resolv.conf <<'EOF'
z /var/spool/postfix/etc/resolv.conf 0644 root root -
EOF

systemd-tmpfiles --create /etc/tmpfiles.d/postfix-chroot-resolv.conf
postfix checkLangage du code : JavaScript (javascript)

Ce n’était pas bloquant, mais autant nettoyer. Les warnings Postfix ont une fâcheuse tendance à devenir des emails cron à 3h du matin.

Postfix et Dovecot LMTP : les mails locaux de root restent en queue

Après le redémarrage de Postfix, la queue contenait encore deux messages :

mailq

Sortie :

MAILER-DAEMON
(host apollo.skyminds.net[private/dovecot-lmtp] said:
451 4.3.0 <root@apollo.skyminds.net> Temporary internal error)
root@apollo.skyminds.netLangage du code : CSS (css)

Postfix était bien actif, envoyait correctement vers Gmail, mais les messages locaux destinés à root@apollo.skyminds.net passaient par Dovecot LMTP.

La configuration expliquait le comportement :

postconf -n | grep -E '^(alias_maps|alias_database|local_recipient_maps|mailbox_transport|local_transport|virtual_alias_maps|virtual_mailbox_maps|mydestination)'Langage du code : JavaScript (javascript)

Résultat :

mailbox_transport = lmtp:unix:private/dovecot-lmtp
mydestination = $myhostname, localhost.$mydomain, localhost
virtual_alias_maps = proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_maps.cf, proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_maps.cf, proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf
virtual_mailbox_maps = proxy:mysql:/etc/postfix/sql/mysql_virtual_mailbox_maps.cf, proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cfLangage du code : JavaScript (javascript)

La directive importante est celle-ci :

mailbox_transport = lmtp:unix:private/dovecot-lmtpLangage du code : PHP (php)

Même le courrier local pour root partait vers Dovecot LMTP. Or root n’est pas une boîte virtuelle Dovecot/MySQL valide. Résultat : Dovecot refusait la livraison.

La solution propre consiste à rediriger le courrier de root vers une vraie adresse email.

Dans /etc/aliases :

nano /etc/aliases

Ajouter ou corriger :

root: skyminds@example.comLangage du code : CSS (css)

Puis reconstruire la base d’aliases :

newaliases
postfix reload

Test :

echo "Test root alias from apollo $(date)" | mail -s "Apollo root mail test" root
journalctl -u postfix -n 80 --no-pager -lLangage du code : PHP (php)

On veut voir une livraison vers l’adresse réelle :

orig_to=<root>
to=<skyminds@example.com>
status=sentLangage du code : HTML, XML (xml)

Si l’alias n’est pas pris en compte, vérifiez la configuration effective :

postconf alias_maps alias_database mailbox_transport local_transport

Puis forcez les alias classiques :

postconf -e 'alias_maps = hash:/etc/aliases'
postconf -e 'alias_database = hash:/etc/aliases'
newaliases
postfix reloadLangage du code : JavaScript (javascript)

Une fois le test validé, on peut supprimer les vieux messages bloqués dans la queue :

postsuper -d B0F2418016A
postsuper -d 41606180167
mailq

Postfix : warning NIS sans importance

Dans les logs Postfix, j’avais aussi ce message :

warning: dict_nis_init: NIS domain name not set - NIS lookups disabledLangage du code : JavaScript (javascript)

Ce n’est pas bloquant. Cela signifie généralement que Postfix a une map d’aliases qui référence encore NIS, alors que le serveur n’utilise pas NIS.

On vérifie :

postconf alias_maps

Si la sortie contient quelque chose comme :

alias_maps = hash:/etc/aliases, nis:mail.aliasesLangage du code : JavaScript (javascript)

on peut nettoyer :

postconf -e 'alias_maps = hash:/etc/aliases'
postconf -e 'alias_database = hash:/etc/aliases'
newaliases
postfix reload
postfix checkLangage du code : JavaScript (javascript)

Ce n’était pas le problème principal, mais c’est le genre de petit warning qu’il vaut mieux éliminer pendant qu’on a encore la tête dans les logs.

Problème 5 : avec logrotate, doublon cloud-init après migration

Après la migration, systemctl --failed listait encore :

logrotate.service loaded failed failed Rotate log filesLangage du code : CSS (css)

En inspectant le service :

systemctl status logrotate.service --no-pager -l
journalctl -u logrotate -b --no-pager -lLangage du code : CSS (css)

j’ai obtenu :

error: cloud-init-base:1 duplicate log entry for /var/log/cloud-init-output.log
error: found error in file cloud-init-base, skippingLangage du code : HTTP (http)

On recherche :

grep -R "cloud-init" /etc /usr/lib /usr/share -n 2>/dev/null | grep -i logrotateLangage du code : JavaScript (javascript)

Résultat :

/etc/logrotate.d/cloud-init:1:/var/log/cloud-init*.log
/etc/logrotate.d/cloud-init-base:1:/var/log/cloud-init*.logLangage du code : JavaScript (javascript)

Les deux fichiers déclaraient le même pattern :

/var/log/cloud-init*.logLangage du code : JavaScript (javascript)

Et ce pattern couvre bien :

/var/log/cloud-init-output.log
/var/log/cloud-init.logLangage du code : JavaScript (javascript)

logrotate refuse donc de continuer, car deux règles essaient de gérer les mêmes fichiers.

Correction : sauvegarder les deux fichiers, puis désactiver l’ancien doublon.

backup_dir="/root/backup-logrotate-$(date +%F-%H%M)"
mkdir -p "$backup_dir"

cp -a /etc/logrotate.d/cloud-init /etc/logrotate.d/cloud-init-base "$backup_dir"/
mv /etc/logrotate.d/cloud-init "$backup_dir/cloud-init.disabled"Langage du code : JavaScript (javascript)

Ensuite, simulation :

logrotate -d /etc/logrotate.conf

Le mode debug affiche ce que logrotate ferait, mais ne modifie rien.

Si tout est propre, on lance le vrai run :

logrotate -v /etc/logrotate.conf

Puis on nettoie l’état systemd :

systemctl reset-failed logrotate
systemctl --failed

Après correction, logrotate -d ne remontait plus d’erreur et lisait seulement :

reading config file cloud-init-base

Petit bug typique de migration : deux fichiers se marchent dessus, et logrotate boude.

Problème 6 : avec acme.sh, le hook Cloudflare/TLSA n’a pas son token dans cron

Autre email cron reçu après migration :

Cron <root@apollo> "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

[ERR ] CLOUDFLARE_API_TOKEN is not set in the environment.
[ERR ] Make sure it is exported in ~/.bashrc, or define it in /etc/update-tlsa.conf.
[Sat Apr 25 00:52:53 CEST 2026] Reload error for: example.com
[Sat Apr 25 00:52:53 CEST 2026] Error renewing example.com_ecc.
Langage du code : JavaScript (javascript)

Ici, le problème ne venait pas directement du certificat. acme.sh lançait bien le renouvellement, mais le hook de reload échouait.

Pour identifier le hook :

grep -R "example.com\|Le_ReloadCmd\|TLSA\|update-tlsa\|cloudflare" /root/.acme.sh/example.com_ecc /root/.acme.sh/account.conf -n 2>/dev/nullLangage du code : JavaScript (javascript)

Résultat :

/root/.acme.sh/example.com_ecc/example.com.conf:19:Le_ReloadCmd='__ACME_BASE64__START_L2hvbWUvc2NyaXB0cy9yZWxvYWQtYW5kLXRsc2Euc2g=__ACME_BASE64__END_'Langage du code : JavaScript (javascript)

Cette valeur est encodée en base64 par acme.sh. Une fois décodée, elle correspond à :

/home/scripts/reload-and-tlsa.sh

Le hook recharge les services et met à jour les enregistrements TLSA via Cloudflare. Problème : cron utilise un environnement minimal. Il ne charge pas automatiquement /root/.bashrc, /root/.profile, ni les variables exportées dans un shell interactif.

Donc cette variable n’existait pas pendant le cron :

CLOUDFLARE_API_TOKEN

La solution propre consiste à stocker le token dans un fichier dédié :

nano /etc/update-tlsa.conf

Contenu :

CLOUDFLARE_API_TOKEN="votre_token_cloudflare"Langage du code : JavaScript (javascript)

Puis sécuriser le fichier :

chown root:root /etc/update-tlsa.conf
chmod 600 /etc/update-tlsa.conf

Ensuite, le hook doit charger ce fichier. Dans /home/scripts/reload-and-tlsa.sh, ajouter en haut du script, juste après le shebang :

if [ -f /etc/update-tlsa.conf ]; then
    # shellcheck disable=SC1091
    . /etc/update-tlsa.conf
fi

export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:-}"Langage du code : PHP (php)

Pour vérifier sans afficher le secret :

set -a
. /etc/update-tlsa.conf
set +a

echo "${CLOUDFLARE_API_TOKEN:+TOKEN_LOADED}"Langage du code : JavaScript (javascript)

La sortie attendue :

TOKEN_LOADED

Ensuite, tester le hook directement :

/home/scripts/reload-and-tlsa.sh

ou, si le script attend un domaine :

/home/scripts/reload-and-tlsa.sh example.com

Puis tester le renouvellement :

"/root/.acme.sh"/acme.sh --renew -d example.com --ecc --home "/root/.acme.sh" --forceLangage du code : JavaScript (javascript)

Et enfin le cron complet, sans masquer la sortie :

"/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh"Langage du code : JavaScript (javascript)

En résumé : le certificat n’était pas le vrai problème. Le renouvellement appelait un hook qui dépendait d’un token Cloudflare absent de l’environnement cron.

Problème 7 : rediriger les emails cron de root et www-data

Après une migration, les emails cron deviennent soudain très bavards. C’est utile au début, puis vite pénible si root et www-data reçoivent tout localement.

J’ai donc ajouté des aliases propres :

nano /etc/aliases

Exemple :

root: skyminds@example.com
www-data: skyminds@example.comLangage du code : CSS (css)

Puis :

newaliases
postfix reload

Test :

echo "Test root alias from apollo $(date)" | mail -s "Apollo root alias test" root
echo "Test www-data alias from apollo $(date)" | mail -s "Apollo www-data alias test" www-dataLangage du code : PHP (php)

Puis vérifier :

journalctl -u postfix -n 100 --no-pager -l

Cela évite que des erreurs importantes restent coincées dans une boîte locale que personne ne lit.

Checklist finale après corrections

Une fois les corrections appliquées, j’ai relancé une vérification complète :

systemctl --failed

L’objectif :

0 loaded units listed.

Puis :

systemctl status nginx --no-pager -l
systemctl status postfix --no-pager -l
systemctl status dovecot --no-pager -l
systemctl status php8.3-fpm --no-pager -l
systemctl status mysql --no-pager -l
systemctl status logrotate.timer --no-pager -lLangage du code : CSS (css)

Côté Postfix :

postfix check
mailq
ss -ltnp | grep master

L’objectif :

Mail queue is emptyLangage du code : PHP (php)

et des ports SMTP/Submission bien ouverts :

:25
:587Langage du code : CSS (css)

Côté logs :

journalctl -p err -b --no-pager
journalctl -u postfix -n 100 --no-pager -l
journalctl -u logrotate -b --no-pager -l

La migration n’est terminée que lorsque les services sont actifs, les timers sont propres, la queue mail est vide, les crons ne crient plus, et systemctl --failed ne retourne plus rien. Sinon, ce n’est pas une migration finie. C’est une migration qui attend son prochain email à 3h du matin.

Nettoyer après migration

Une fois les services réparés, je fais le ménage.

Supprimer les paquets inutiles

sudo apt autoremove --purge
sudo apt clean

Chercher les fichiers de configuration hérités

sudo find /etc -type f \( -name "*.dpkg-dist" -o -name "*.dpkg-old" -o -name "*.ucf-dist" \) -printLangage du code : PHP (php)

Comparez proprement :

sudo diff -u /etc/nginx/nginx.conf /etc/nginx/nginx.conf.dpkg-dist

Adaptez selon les fichiers trouvés.

Vérifier les services activés

systemctl list-unit-files --state=enabledLangage du code : PHP (php)

Puis les services échoués :

systemctl --failed

Vérifier les ports ouverts

sudo ss -ltnp

Sur un serveur web/mail, je veux retrouver au minimum :

22    SSH
25    SMTP
80    HTTP
443   HTTPS
465   SMTPS
587   Submission
993   IMAPS

Selon votre configuration, vous pouvez aussi avoir :

143   IMAP
110   POP3
995   POP3S
3306  MySQL local ou privé uniquement
6379  Redis local uniquement

Vérifier les tâches cron et timers systemd

systemctl list-timers --all
sudo crontab -l
ls -la /etc/cron.*Langage du code : PHP (php)

Si vous utilisez acme.sh, vérifiez aussi :

crontab -l | grep acme

Et testez que vos variables d’environnement critiques sont encore disponibles, surtout si vous utilisez Cloudflare pour les DNS ou les certificats.

Vérifications spécifiques WordPress

Sur un serveur WordPress, je termine avec quelques tests applicatifs.

PHP-FPM

php -v
systemctl status php8.3-fpm --no-pager
systemctl status php8.4-fpm --no-pager
systemctl status php8.5-fpm --no-pagerLangage du code : CSS (css)

Selon vos pools :

ls -la /etc/php/*/fpm/pool.d/

Puis :

sudo journalctl -u php8.3-fpm -b --no-pagerLangage du code : CSS (css)

WP-CLI

cd /home/www/example.com/public_html
wp core version --allow-root
wp plugin list --allow-root
wp theme list --allow-rootLangage du code : PHP (php)

Nginx + PHP

Testez une page PHP :

curl -I https://example.com/Langage du code : JavaScript (javascript)

Puis vérifiez les headers :

curl -sI https://example.com/ | egrep -i 'HTTP/|server|cache-control|cf-cache-status|content-encoding|x-cache'Langage du code : JavaScript (javascript)

WooCommerce

Pour WooCommerce, je teste toujours :

  • Page boutique
  • Page catégorie produit
  • Page produit
  • Ajout panier
  • Mini panier
  • Checkout invité
  • Checkout connecté
  • Mon compte
  • Commandes
  • Webhooks
  • Emails transactionnels

Si Cloudflare est devant le site, je vérifie aussi que les pages panier, checkout et compte ne sont pas servies depuis le cache.

Commandes utiles en résumé

Migration

sudo apt update
sudo apt full-upgrade
sudo apt autoremove --purge
sudo reboot

sudo apt install update-manager-core screen
screen -S ubuntu-upgrade

sudo do-release-upgradeLangage du code : JavaScript (javascript)

Diagnostic global

lsb_release -a
uname -a
systemctl --failed
journalctl -p err -b --no-pager

Nginx

sudo nginx -t
sudo journalctl -u nginx -b --no-pager
apt-cache search brotli | grep -i nginx
sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
sudo systemctl restart nginxLangage du code : JavaScript (javascript)

Dovecot

sudo doveconf -n
sudo dovecot -n
sudo journalctl -u dovecot -b --no-pager
sudo systemctl restart dovecot

Postfix

postconf -n
sudo journalctl -u postfix -b --no-pager
sudo postfix check
sudo systemctl restart postfix

Conclusion

La migration d’Ubuntu 24.04 LTS vers Ubuntu 26.04 LTS est plutôt propre, mais elle n’est pas neutre pour un serveur configuré à la main.

Le système lui-même se met à jour correctement. En revanche, les services historiques comme Nginx, Dovecot et Postfix peuvent révéler des configurations anciennes, des modules manquants ou des directives devenues obsolètes.

Dans mon cas :

  • Nginx ne chargeait plus Brotli tant que le module libnginx-mod-http-brotli n’était pas installé.
  • Dovecot exigeait une configuration adaptée à la branche 2.4.
  • Postfix signalait des directives TLS historiques à remplacer.

Rien de dramatique, donc. Mais il faut prévoir du temps, garder une session screen, avoir des sauvegardes complètes, et ne pas redémarrer dix services au hasard comme un DJ sous caféine.

Ubuntu 26.04 LTS est une bonne base pour les prochaines années. Mais comme toujours avec les serveurs : la mise à jour n’est que la moitié du travail. L’autre moitié, c’est lire les logs.

Bonne migration !

Demandez à l'IA son opinion
Gravatar for Matt Biscay

Je suis Matt Biscay, développeur WordPress & WooCommerce certifié chez Codeable, administrateur système et enseignant.

J’aide les entreprises à créer, optimiser et fiabiliser leurs sites WordPress avec une approche technique propre : performance, sécurité, maintenance, développement sur mesure et résolution de problèmes complexes.

Sur Skyminds, je partage des tutoriels WordPress, WooCommerce, Linux et administration système, avec des solutions testées sur des cas réels et pensées pour durer.

Découvrez mes services WordPress et WooCommerce.

Opinions