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-oldet.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-brotlin’é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.