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”.

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-pagerCode language: 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 /etcCode language: 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).txtCode language: 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/nullCode language: 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/phpCode language: 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/nullCode language: 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).sqlCode language: JavaScript (javascript)

Ensuite, je copie tout cela hors du serveur.

rsync -avz /root/backup-before-ubuntu-26.04/ user@backup-server:/backups/apollo/Code language: 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 -hCode language: 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}'Code language: 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 {} \;Code language: 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.

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-upgradeCode language: 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 -dCode language: 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" \) -printCode language: 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-pagerCode language: 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"Code language: 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 -nCode language: JavaScript (javascript)

Vérifiez les modules Nginx disponibles :

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

Puis les modules chargés :

grep -R "^[[:space:]]*load_module" /etc/nginx /usr/share/nginx 2>/dev/nullCode language: 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 - staticCode language: JavaScript (javascript)

Installez-les :

sudo apt update
sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-staticCode language: 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'Code language: JavaScript (javascript)

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

content-encoding: brCode language: HTTP (http)

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

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_versionCode language: HTTP (http)

Diagnostiquer proprement

Lancez :

sudo doveconf -n

Puis :

sudo dovecot -n

Ensuite, inspectez les fichiers actifs :

sudo grep -R "^[^#]" /etc/dovecot/ -nCode language: 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.pemCode language: 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.pemCode language: 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.pemCode language: 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.pemCode language: 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/%uCode language: JavaScript (javascript)

Qui devient :

mail_driver = mbox
mail_path = ~/mail
mail_inbox_path = /var/spool/mail/%{user}Code language: 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 -nCode language: 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.comCode language: 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'Code language: 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_tlsCode language: 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.pemCode language: 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.pemCode language: 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'Code language: 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.comCode language: CSS (css)

Test SMTPS sur 465 :

openssl s_client -connect localhost:465 -servername mail.example.comCode language: CSS (css)

Puis logs :

journalctl -u postfix -b --no-pager

Et configuration finale :

postconf -n

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.XXXCode language: 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 -lCode language: JavaScript (javascript)

On vérifie ensuite :

postconf -n | grep '^inet_interfaces'
ss -ltnp | grep masterCode language: 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.Code language: 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 missedCode language: PHP (php)

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

postfix.serviceCode language: CSS (css)

On peut le vérifier avec :

systemctl status postfix --no-pager -l
systemctl list-unit-files 'postfix*'
systemctl list-units 'postfix*' --allCode language: 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 checkCode language: 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/nullCode language: JavaScript (javascript)

Après modification :

systemctl restart postfix.service
systemctl reset-failed postfix.service
systemctl --failedCode language: 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.confCode language: 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.confCode language: JavaScript (javascript)

Résultat :

-rw-r--r-- 1 systemd-resolve systemd-resolve 920 Apr 24 00:50 /var/spool/postfix/etc/resolv.confCode language: 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 postfixCode language: 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 checkCode language: 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.netCode language: 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)'Code language: 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.cfCode language: JavaScript (javascript)

La directive importante est celle-ci :

mailbox_transport = lmtp:unix:private/dovecot-lmtpCode language: 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.comCode language: 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 -lCode language: PHP (php)

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

orig_to=<root>
to=<skyminds@example.com>
status=sentCode language: 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 reloadCode language: 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 disabledCode language: 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.aliasesCode language: JavaScript (javascript)

on peut nettoyer :

postconf -e 'alias_maps = hash:/etc/aliases'
postconf -e 'alias_database = hash:/etc/aliases'
newaliases
postfix reload
postfix checkCode language: 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 filesCode language: CSS (css)

En inspectant le service :

systemctl status logrotate.service --no-pager -l
journalctl -u logrotate -b --no-pager -lCode language: 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, skippingCode language: HTTP (http)

On recherche :

grep -R "cloud-init" /etc /usr/lib /usr/share -n 2>/dev/null | grep -i logrotateCode language: 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*.logCode language: JavaScript (javascript)

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

/var/log/cloud-init*.logCode language: JavaScript (javascript)

Et ce pattern couvre bien :

/var/log/cloud-init-output.log
/var/log/cloud-init.logCode language: 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"Code language: 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.
Code language: 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/nullCode language: JavaScript (javascript)

Résultat :

/root/.acme.sh/example.com_ecc/example.com.conf:19:Le_ReloadCmd='__ACME_BASE64__START_L2hvbWUvc2NyaXB0cy9yZWxvYWQtYW5kLXRsc2Euc2g=__ACME_BASE64__END_'Code language: 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"Code language: 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:-}"Code language: PHP (php)

Pour vérifier sans afficher le secret :

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

echo "${CLOUDFLARE_API_TOKEN:+TOKEN_LOADED}"Code language: 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" --forceCode language: JavaScript (javascript)

Et enfin le cron complet, sans masquer la sortie :

"/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh"Code language: 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.comCode language: 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-dataCode language: 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 -lCode language: CSS (css)

Côté Postfix :

postfix check
mailq
ss -ltnp | grep master

L’objectif :

Mail queue is emptyCode language: PHP (php)

et des ports SMTP/Submission bien ouverts :

:25
:587Code language: 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" \) -printCode language: 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=enabledCode language: 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.*Code language: 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-pagerCode language: CSS (css)

Selon vos pools :

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

Puis :

sudo journalctl -u php8.3-fpm -b --no-pagerCode language: 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-rootCode language: PHP (php)

Nginx + PHP

Testez une page PHP :

curl -I https://example.com/Code language: 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'Code language: 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-upgradeCode language: 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 nginxCode language: 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 !

Vous souhaitez enrichir votre site avec de nouvelles fonctionnalités ? Ensemble, donnons vie à vos idées, simplement et efficacement.

Parlons de vos besoins spécifiques »

Gravatar for Matt Biscay

Développeur certifié WordPress & WooCommerce chez Codeable, administrateur système et enseignant-chercheur, je mets mon expertise au service de vos projets web.

Ma priorité : des sites performants, fiables et sécurisés, pensés pour offrir la meilleure expérience utilisateur. J’accompagne chaque client avec écoute et pédagogie, pour transformer vos idées en solutions concrètes et durables.

Profitez de solutions WordPress et WooCommerce sur-mesure, pensées pour optimiser durablement votre site.
Explorez les leviers pour booster l’impact de votre site web.

Opinions