Gérer les services sur les distributions Linux modernes signifie travailler avec systemd, le système d’initialisation et gestionnaire de services devenu le standard sur la plupart des distributions. Ce guide couvre tout ce que vous devez savoir sur la création d’unités de service personnalisées, la gestion des dépendances entre services, l’analyse des logs avec journalctl, le remplacement de cron par les timers systemd et le diagnostic des problèmes lorsque les services ne démarrent pas ou s’arrêtent de manière inattendue.

Prérequis

  • Une distribution Linux exécutant systemd (Ubuntu 16.04+, CentOS 7+, Debian 8+, Fedora 15+)
  • Accès au terminal avec privilèges sudo
  • Compréhension basique des permissions de fichiers Linux et de la gestion des processus
  • Un éditeur de texte installé (vim, nano ou similaire)

Comprendre les Unités Systemd

Systemd organise tout en unités — des ressources que le système sait gérer. Les types d’unités les plus courants sont :

Type d’UnitéExtensionObjectif
Service.serviceProcessus daemon et tâches ponctuelles
Timer.timerExécution planifiée (remplace cron)
Socket.socketActivation de sockets IPC et réseau
Mount.mountPoints de montage du système de fichiers
Target.targetGroupes d’unités (comme les runlevels)

Les fichiers d’unité résident dans trois emplacements, avec priorité décroissante :

/etc/systemd/system/      # Unités personnalisées de l'admin (priorité maximale)
/run/systemd/system/       # Unités d'exécution
/lib/systemd/system/       # Unités par défaut de la distribution

Pour lister toutes les unités chargées et leurs états :

systemctl list-units --type=service
systemctl list-units --type=service --state=failed

Création d’Unités de Service Personnalisées

Un fichier d’unité de service bien structuré comporte trois sections. Voici un exemple complet pour une application Node.js :

# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
Documentation=https://example.com/docs
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStartPre=/usr/bin/node --check /opt/myapp/server.js
ExecStart=/usr/bin/node /opt/myapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# Renforcement de la sécurité
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Directives clés dans la section [Service] :

  • Type : simple (défaut, processus au premier plan), forking (pour les daemons qui font un fork), oneshot (s’exécute une fois et se termine), notify (signale la disponibilité via sd_notify)
  • ExecStartPre : Commande pour exécuter une validation avant de démarrer le processus principal
  • Restart : on-failure, always, on-abnormal ou no
  • RestartSec : Délai entre les tentatives de redémarrage en secondes

Après avoir créé le fichier, chargez-le et démarrez-le :

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
sudo systemctl status myapp.service

Gestion des Dépendances

Systemd utilise plusieurs directives pour exprimer les relations entre unités :

[Unit]
# Ordonnancement (quand démarrer, pas si)
After=network-online.target    # Démarrer après que le réseau soit prêt
Before=nginx.service           # Démarrer avant nginx

# Force de la dépendance
Requires=postgresql.service    # Dépendance forte — échoue si postgres échoue
Wants=redis.service            # Dépendance faible — continue si redis échoue
BindsTo=docker.service         # Cycle de vie lié — s'arrête quand docker s'arrête

# Résolution de conflits
Conflicts=iptables.service     # Ne peut pas fonctionner avec iptables

La différence entre After et Requires est critique. After contrôle l’ordonnancement (séquence) tandis que Requires contrôle l’activation (s’il faut inclure une dépendance). Vous avez généralement besoin des deux :

Requires=postgresql.service
After=postgresql.service

Utiliser Requires seul ne garantit pas l’ordre — les deux services peuvent démarrer simultanément. Utiliser After seul n’inclut pas la dépendance — il les ordonne seulement si les deux sont en cours de démarrage.

Pour visualiser l’arbre de dépendances d’un service :

systemctl list-dependencies myapp.service
systemctl list-dependencies myapp.service --reverse

Analyse des Logs avec Journalctl

Journalctl est l’outil pour interroger le journal systemd. Voici les modèles les plus utiles :

# Suivre les logs d'un service spécifique en temps réel
journalctl -u myapp.service -f

# Afficher les logs depuis le dernier démarrage
journalctl -u myapp.service -b

# Filtrer par plage horaire
journalctl -u myapp.service --since "2026-02-20 08:00" --until "2026-02-20 18:00"

# Afficher uniquement les erreurs et au-dessus
journalctl -u myapp.service -p err

# Sortie en format JSON pour traitement
journalctl -u myapp.service -o json-pretty --no-pager

# Afficher les messages du noyau liés aux OOM kills
journalctl -k --grep="Out of memory"

# Vérifier l'utilisation disque du journal
journalctl --disk-usage

# Nettoyer les anciens logs pour libérer de l'espace
sudo journalctl --vacuum-time=7d
sudo journalctl --vacuum-size=500M

Les niveaux de priorité suivent la convention syslog : emerg (0), alert (1), crit (2), err (3), warning (4), notice (5), info (6), debug (7). Utiliser -p err affiche err et tout ce qui est plus sévère.

Pour configurer le stockage persistant du journal (survit aux redémarrages) :

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

Timers Systemd

Les timers systemd remplacent cron avec une meilleure journalisation, gestion des dépendances et fiabilité. Un timer se compose de deux fichiers — le .timer et son .service associé :

# /etc/systemd/system/backup.timer
[Unit]
Description=Daily database backup timer

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=900
Persistent=true
Unit=backup.service

[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Database backup job

[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup-db.sh
StandardOutput=journal

Expressions OnCalendar courantes :

ExpressionSignification
*-*-* 02:00:00Quotidiennement à 2h du matin
Mon *-*-* 09:00:00Chaque lundi à 9h
*-*-01 00:00:00Premier jour de chaque mois
*-*-* *:00/15:00Toutes les 15 minutes
hourlyToutes les heures (raccourci)

Activer et gérer les timers :

sudo systemctl enable --now backup.timer
systemctl list-timers --all
systemctl status backup.timer

La directive Persistent=true garantit que si le système était éteint lorsque le timer devait se déclencher, il exécute le travail immédiatement au prochain démarrage.

Comparaison : Systemd vs SysVinit vs Upstart

FonctionnalitésystemdSysVinitUpstart
Démarrage parallèleOuiNonPartiel
Gestion des dépendancesDirectives déclarativesOrdonnancement manuel par numérosBasé sur événements
Supervision des servicesPolitiques de redémarrage intégréesAucune (nécessite outils externes)Directive respawn
Journalisationjournald (structuré, indexé)syslog (fichiers texte brut)syslog
Activation par socketOuiNonNon
Contrôle des ressourcesIntégration cgroupsAucunAucun
Minuteurs/planificationTimers intégrésNécessite cronNécessite cron
Format de configurationFichiers d’unité style INIScripts shellFichiers conf à stanzas
Analyse du démarragesystemd-analyzeAucunAucun
Statut d’adoptionPar défaut sur la plupart des distrosSystèmes héritésAbandonné

Scénario Réel

Vous avez un serveur de production exécutant une API Python qui dépend de PostgreSQL et Redis. L’API doit démarrer après que les deux bases de données soient prêtes, redémarrer en cas de crash avec une stratégie de back-off, et exécuter un travail de nettoyage toutes les 6 heures.

Unité de service (/etc/systemd/system/api.service) :

[Unit]
Description=Production Python API
After=network-online.target postgresql.service redis.service
Requires=postgresql.service
Wants=redis.service

[Service]
Type=simple
User=apiuser
WorkingDirectory=/opt/api
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/api/venv/bin/gunicorn -w 4 -b 0.0.0.0:8000 app:create_app()
Restart=on-failure
RestartSec=10
StartLimitIntervalSec=300
StartLimitBurst=5

[Install]
WantedBy=multi-user.target

Timer de nettoyage (/etc/systemd/system/api-cleanup.timer) :

[Unit]
Description=API cleanup every 6 hours

[Timer]
OnBootSec=15min
OnUnitActiveSec=6h
Persistent=true

[Install]
WantedBy=timers.target

Cette configuration garantit que l’API ne démarre que lorsque PostgreSQL est en cours d’exécution (dépendance forte) et utilise optionnellement Redis (dépendance faible). La politique de redémarrage permet 5 tentatives en 5 minutes avant d’abandonner.

Pièges et Cas Particuliers

  • ExecStart doit utiliser des chemins absolus : Les chemins relatifs causent des échecs silencieux. Utilisez toujours /usr/bin/node, pas node
  • Type=forking nécessite PIDFile : Si votre daemon fait un fork, systemd doit suivre le PID enfant. Définissez PIDFile=/run/myapp.pid et assurez-vous que votre app l’écrit
  • Fichiers Environment vs inline : Pour de nombreuses variables, utilisez EnvironmentFile=/etc/myapp/env au lieu de multiples lignes Environment=. Le format est KEY=value (sans export)
  • Services utilisateur vs services système : Les unités utilisateur dans ~/.config/systemd/user/ s’exécutent sans sudo mais uniquement tant que l’utilisateur est connecté (sauf si loginctl enable-linger est activé)
  • Reload vs restart vs daemon-reload : systemctl restart myapp redémarre le processus. systemctl daemon-reload recharge les définitions des fichiers d’unité. Après modification d’un fichier d’unité, vous devez d’abord exécuter daemon-reload
  • Le target de WantedBy compte : Utilisez multi-user.target pour les serveurs sans interface graphique, graphical.target pour les services de bureau

Résolution de Problèmes

Lorsqu’un service ne démarre pas, suivez ce flux de diagnostic :

# 1. Vérifier l'état actuel et les logs récents
systemctl status myapp.service

# 2. Obtenir des informations détaillées sur l'échec
journalctl -u myapp.service -e --no-pager -n 50

# 3. Valider la syntaxe du fichier d'unité
systemd-analyze verify /etc/systemd/system/myapp.service

# 4. Vérifier les cycles d'ordonnancement
systemd-analyze critical-chain myapp.service

# 5. Tester la commande ExecStart manuellement en tant qu'utilisateur du service
sudo -u appuser /usr/bin/node /opt/myapp/server.js

# 6. Vérifier les refus SELinux ou AppArmor
sudo ausearch -m avc -ts recent 2>/dev/null || sudo journalctl -k --grep="apparmor"

# 7. Vérifier les permissions des fichiers
ls -la /opt/myapp/
namei -l /opt/myapp/server.js

Causes courantes d’échec et corrections :

SymptômeCauseCorrection
code=exited, status=203/EXECBinaire introuvable ou non exécutableVérifiez le chemin avec which, définissez chmod +x
code=exited, status=217/USERL’utilisateur spécifié dans User= n’existe pasCréez l’utilisateur avec useradd -r -s /sbin/nologin
Start request repeated too quicklyBoucle de redémarrage atteignant StartLimitBurstAugmentez RestartSec, corrigez le crash sous-jacent, puis systemctl reset-failed
code=exited, status=200/CHDIRWorkingDirectory n’existe pasCréez le répertoire et corrigez la propriété
Service démarre mais port inaccessiblePare-feu ou adresse de liaison incorrecteVérifiez avec ss -tlnp, vérifiez les règles firewall-cmd ou ufw

Résumé

  • Les fichiers d’unité systemd utilisent des sections déclaratives style INI ([Unit], [Service], [Install]) pour définir le comportement du service
  • Les unités personnalisées vont dans /etc/systemd/system/ — exécutez toujours daemon-reload après les modifications
  • Utilisez Requires= avec After= ensemble pour les dépendances fortes avec ordonnancement correct
  • Journalctl fournit un filtrage puissant par unité, priorité, plage horaire et format de sortie
  • Les timers systemd offrent une meilleure fiabilité que cron avec planification persistante et journalisation par tâche
  • Les directives de renforcement de sécurité comme ProtectSystem=strict et PrivateTmp=true devraient être standard en production
  • Diagnostiquez avec systemctl status, journalctl -u et systemd-analyze verify

Articles Connexes