En 2010, Lennart Poettering a publié un article intitulé "Rethinking PID 1" et a annoncé systemd. La communauté Linux a répondu avec l'enthousiasme habituel qu'elle réserve aux changements - c'est-à-dire une guerre sainte qui dure encore aujourd'hui sur certains forums que je ne nommerai pas.
Les arguments contre systemd sont connus : trop complexe, trop monolithique, fait trop de choses, va à l'encontre de la philosophie Unix du "faire une chose et la faire bien". Ces arguments ne sont pas complètement faux. systemd fait effectivement beaucoup de choses. Il gère les services, les timers, les sockets, les montages, la résolution DNS, les conteneurs, le réseau, les sessions utilisateur, et probablement votre déclaration d'impôts si vous lui donnez le bon fichier de config.
Et pourtant systemd a gagné. Il tourne sur quasiment toutes les distributions Linux modernes, gère des millions de serveurs en prod, et n'est pas près de disparaître. Autant apprendre à s'en servir correctement plutôt que de continuer à lancer des scripts bash avec nohup & en espérant que ça tienne.
Ce guide part du principe que vous savez faire systemctl start/stop/status. On va parler de ce qui vient après.
Les unités - comprendre ce qu'on manipule
systemd gère des unités. Chaque unité est un fichier de configuration qui décrit quelque chose que systemd doit gérer. Les types d'unités que vous rencontrerez :
.service- un processus à lancer et surveiller. C'est 90% de ce qu'on fait..timer- un déclencheur temporel. Le remplacement moderne de cron, avec des logs, des dépendances, et un comportement prévisible..socket- un socket réseau ou Unix. systemd écoute sur le socket et démarre le service seulement quand une connexion arrive. Activation à la demande, zéro ressource consommée au repos..path- un déclencheur sur un chemin filesystem. Le service démarre quand un fichier apparaît ou change. Utile pour les pipelines de traitement de fichiers..target- un groupe d'unités. L'équivalent des runlevels SysV, mais plus flexible.multi-user.target,network.target,graphical.target..mountet.automount- des points de montage. systemd peut gérer vos montages NFS ou CIFS avec les mêmes mécanismes de dépendances que les services..slice- un groupe de ressources. Permet de limiter CPU, mémoire, IO pour un ensemble de services via cgroups.
Les fichiers d'unités se trouvent dans :
/lib/systemd/system/ # unités installées par les paquets
/etc/systemd/system/ # vos unités custom et les overrides
~/.config/systemd/user/ # unités utilisateur (sans root)
La priorité va de /etc vers /lib - vos fichiers dans /etc écrasent ceux des paquets. Pour modifier une unité système sans toucher au fichier original :
systemctl edit nginx
# Ouvre un éditeur sur /etc/systemd/system/nginx.service.d/override.conf
# Seuls les paramètres que vous définissez écrasent l'original
systemctl edit --full nginx
# Copie l'unité complète dans /etc/systemd/system/ pour modification totale
systemctl edit est la bonne façon de modifier une unité. Éditer directement /lib/systemd/system/nginx.service fonctionne jusqu'à la prochaine mise à jour du paquet qui écrase vos modifications. Vous avez tous fait ça au moins une fois.
Écrire une bonne unit service
Une unit service basique que personne n'écrit correctement :
[Unit]
Description=Radar DevOps daily fetch
Documentation=https://rudeops.com
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=radar
Group=radar
WorkingDirectory=/opt/radar
ExecStartPre=/usr/bin/test -f /opt/radar/radar-fetch.js
ExecStart=/usr/bin/node /opt/radar/radar-fetch.js
ExecStartPost=/usr/bin/systemctl --no-block start radar-notify.service
Restart=on-failure
RestartSec=10s
RestartBurstLimit=3
TimeoutStartSec=30
TimeoutStopSec=10
[Install]
WantedBy=multi-user.target
Type= - le paramètre le plus mal compris
Type=simple (défaut) : systemd considère le service comme démarré dès que le processus principal est lancé. Si votre processus fait des initialisations longues avant d'être prêt, systemd pense qu'il est prêt alors qu'il ne l'est pas encore. Les services qui en dépendent démarrent trop tôt.
Type=exec : comme simple mais systemd attend que le exec() soit effectué. Plus précis que simple, préférez-le.
Type=forking : pour les daemons old-school qui forkent et quittent le processus parent. systemd attend que le parent quitte. Nécessite souvent PIDFile= pour que systemd suive le bon processus.
Type=notify : le service envoie une notification via sd_notify() quand il est prêt. Le plus précis - systemd sait exactement quand le service est opérationnel. Nginx, PostgreSQL et beaucoup de services modernes supportent ça.
Type=oneshot : le processus se termine après avoir fait son travail. Pour les scripts qui font une chose et quittent. Avec RemainAfterExit=yes, systemd considère l'unité comme active même après la fin du processus.
Type=idle : attend que tous les autres jobs soient terminés avant de démarrer. Pour les tâches de maintenance qui ne doivent pas interférer avec le démarrage.
Restart= - définir le comportement en cas d'échec
Restart=no # ne jamais redémarrer (défaut)
Restart=always # toujours redémarrer
Restart=on-failure # redémarrer si code de sortie non-zéro ou signal
Restart=on-abnormal # redémarrer sur signal, timeout, watchdog
Restart=on-abort # redémarrer uniquement sur signal non-géré
Restart=on-failure est le bon choix pour la plupart des services. Restart=always redémarre même après un systemctl stop ce qui surprend tout le monde la première fois.
RestartSec=10s # attendre 10s avant de redémarrer
RestartBurstLimit=3 # max 3 redémarrages en burst avant abandon
StartLimitInterval=60s # fenêtre de temps pour le burst limit
StartLimitAction=reboot # action si le burst limit est atteint (reboot, poweroff...)
RestartBurstLimit=3 avec StartLimitInterval=60s : si le service crashe 3 fois en 60 secondes, systemd abandonne et laisse le service en état failed. Sans ça, un service qui crashe instantanément redémarre en boucle infinie et mange votre CPU.
Les dépendances - la nuance qui compte
After=network.target # démarrer après network.target (ordre seulement)
Wants=network-online.target # démarrer network-online.target si pas actif, continuer même s'il échoue
Requires=postgresql.service # requiert postgresql.service, s'arrête si postgresql s'arrête
BindsTo=postgresql.service # comme Requires mais s'arrête aussi si postgresql redémarre
PartOf=app.target # fait partie de app.target, s'arrête avec lui
After= définit l'ordre sans créer de dépendance. Wants= crée une dépendance faible - l'unité démarre même si la dépendance échoue. Requires= crée une dépendance forte - l'unité s'arrête si la dépendance s'arrête. BindsTo= est plus fort encore - l'unité s'arrête même si la dépendance redémarre.
La confusion classique : After=network.target ne signifie pas que le réseau est disponible - ça signifie juste que le target a été atteint. Pour attendre que le réseau soit vraiment opérationnel : After=network-online.target avec Wants=network-online.target.
Les hooks d'exécution
ExecStartPre=/usr/bin/test -f /opt/app/config.yml # vérifier la config avant démarrage
ExecStartPre=-/usr/bin/mkdir -p /var/run/app # le - ignore l'échec
ExecStart=/opt/app/bin/server
ExecStartPost=/usr/bin/systemctl --no-block start app-warmup.service
ExecReload=/bin/kill -HUP $MAINPID # signal de reload
ExecStop=/opt/app/bin/graceful-shutdown
ExecStopPost=/usr/bin/rm -f /var/run/app.pid
Le préfixe - sur ExecStartPre=- ignore l'échec de la commande - le service démarre quand même. Sans le -, un échec de ExecStartPre bloque le démarrage.
Les timers - remplacer cron proprement
Le problème avec cron : pas de logs intégrés, pas de gestion des erreurs, pas de dépendances, et une syntaxe qui oblige tout le monde à aller sur crontab.guru pour vérifier ce que */5 * * * * signifie vraiment.
Les timers systemd résolvent tout ça - au prix d'un peu plus de verbosité.
Un timer systemd = deux fichiers : un .timer et un .service associé.
radar.service
[Unit]
Description=Radar daily fetch
After=network-online.target
[Service]
Type=oneshot
User=radar
WorkingDirectory=/opt/radar
ExecStart=/usr/bin/node /opt/radar/radar-fetch.js
StandardOutput=journal
StandardError=journal
radar.timer
[Unit]
Description=Run radar fetch every weekday at 6am
Requires=radar.service
[Timer]
OnCalendar=Mon-Fri 06:00:00
RandomizedDelaySec=300
Persistent=true
AccuracySec=1s
[Install]
WantedBy=timers.target
systemctl enable --now radar.timer
systemctl list-timers --all # voir tous les timers et leur prochaine exécution
OnCalendar= - la syntaxe qui remplace crontab.guru
OnCalendar=daily # tous les jours à minuit
OnCalendar=weekly # tous les lundis à minuit
OnCalendar=Mon-Fri 06:00:00 # jours ouvrés à 6h
OnCalendar=*-*-1 00:00:00 # premier jour de chaque mois
OnCalendar=*:0/15 # toutes les 15 minutes
OnCalendar=hourly # toutes les heures
OnCalendar=2026-05-13 08:00:00 # une date précise
Vérifier la syntaxe avant de déployer :
systemd-analyze calendar "Mon-Fri 06:00:00"
# Normalized form: Mon,Tue,Wed,Thu,Fri *-*-* 06:00:00
# Next elapse: Mon 2026-06-08 06:00:00 CEST
OnBootSec= et OnActiveSec= - les timers monotones
# Démarrer 5 minutes après le boot
OnBootSec=5min
# Répéter toutes les 30 minutes après le démarrage du timer
OnActiveSec=30min
# Les deux ensemble : 5 minutes après le boot, puis toutes les heures
OnBootSec=5min
OnUnitActiveSec=1h
Les timers monotones sont relatifs à un événement (boot, démarrage du timer) plutôt qu'à une heure calendaire. Utiles pour les tâches de maintenance qui doivent tourner régulièrement sans contrainte d'heure précise.
RandomizedDelaySec= - éviter les thundering herds
Si vous avez 50 serveurs qui exécutent le même timer à 06:00:00 exactement, ils frappent tous votre base de données ou votre API en même temps. RandomizedDelaySec=300 ajoute un délai aléatoire entre 0 et 5 minutes. Le timer s'exécute entre 6h00 et 6h05, réparti aléatoirement sur vos serveurs. Votre DBA vous remerciera.
Persistent=true - ne pas rater un timer
Si votre machine était éteinte ou en maintenance à l'heure prévue du timer, Persistent=true déclenche le timer au prochain démarrage. Sans ça, le timer rate silencieusement et vous découvrez le lendemain que votre backup de la nuit n'a pas tourné.
Débugger un timer qui ne se déclenche pas
# Voir tous les timers et leur prochaine exécution
systemctl list-timers --all
# Voir les logs du timer
journalctl -u radar.timer
# Voir les logs du service associé
journalctl -u radar.service
# Tester le service manuellement sans attendre le timer
systemctl start radar.service
La sécurité - sandboxer vos services
Le problème par défaut : la plupart des services tournent avec plus de privilèges qu'ils n'en ont besoin. Un service compromis peut lire vos fichiers, écrire partout, et escalader ses privilèges. systemd offre un ensemble de directives de sandboxing qui limitent ce qu'un service peut faire, sans Docker ni namespace manuel.
Utilisateur dédié
User=radar
Group=radar
Le minimum. Votre service ne tourne pas en root. Si quelqu'un compromet le service, il se retrouve avec un utilisateur sans privilèges.
DynamicUser=yes - utilisateur éphémère
DynamicUser=yes
systemd crée un utilisateur avec un UID aléatoire pour la durée du service et le supprime à l'arrêt. Pas besoin de créer un utilisateur système manuellement. Pas d'utilisateur persistant à gérer. Idéal pour les services oneshot ou les tâches de maintenance.
Isolation du filesystem
PrivateTmp=yes
# /tmp et /var/tmp privés, invisibles des autres services
ProtectSystem=strict
# Filesystem en lecture seule sauf /dev, /proc, /sys
ProtectHome=yes
# /home, /root, /run/user inaccessibles
ReadWritePaths=/var/lib/radar /var/log/radar
# Exceptions en écriture pour les dossiers nécessaires
ReadOnlyPaths=/etc/radar
# Accès lecture seule à la config
Isolation réseau et capabilities
NoNewPrivileges=yes
# Interdit l'escalade de privilèges via setuid/setgid
CapabilityBoundingSet=
# Supprime toutes les capabilities Linux
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# Ou garder uniquement ce qui est nécessaire
PrivateNetwork=yes
# Namespace réseau privé - le service ne voit pas le réseau
# À utiliser seulement si le service n'a pas besoin du réseau
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# Restreindre les familles de sockets autorisées
Protection système avancée
ProtectKernelTunables=yes
# Interdit la modification des paramètres kernel via /proc/sys
ProtectKernelModules=yes
# Interdit le chargement de modules kernel
ProtectControlGroups=yes
# Interdit la modification des cgroups
LockPersonality=yes
# Interdit le changement de personality (ABI)
RestrictRealtime=yes
# Interdit la planification temps réel
MemoryDenyWriteExecute=yes
# Interdit les mappings mémoire à la fois write et execute
# Rend les attaques JIT et shellcode plus difficiles
SystemCallFilter=@system-service
# Restreindre les syscalls autorisés à ceux d'un service classique
Vérifier le niveau de sécurité d'une unité
systemd-analyze security radar.service
systemd analyse l'unité et donne un score de 0 (sécurisé) à 10 (non sandboxé). Avec une unité basique sans sandboxing, vous obtenez un score proche de 9.5 accompagné d'une liste de tout ce que vous devriez activer. C'est une checklist gratuite.
Un exemple de sortie :
NAME DESCRIPTION
✗ PrivateNetwork= Service has access to the host network
✗ User=/DynamicUser= Service runs as root user
✗ NoNewPrivileges= Service processes may acquire new privileges
✗ ProtectSystem= Service has full access to the OS file hierarchy
→ Overall exposure level for radar.service: 9.4 UNSAFE 😨
Le but n'est pas forcément d'atteindre 0 - certaines directives sont incompatibles avec ce que votre service doit faire. Mais descendre sous 4 est raisonnable pour la plupart des services.
Débugger un service qui crashe
La première commande
systemctl status mon-service
Tout le monde la connaît. Ce que personne ne lit : les dernières lignes de log affichées en bas. Elles contiennent souvent l'erreur exacte. Regardez-les avant d'aller chercher ailleurs.
journalctl - aller plus loin
# Logs du service depuis le dernier démarrage
journalctl -u mon-service
# Suivre en temps réel
journalctl -u mon-service -f
# Depuis le dernier boot
journalctl -u mon-service -b
# Depuis un boot précédent (utile si le serveur a redémarré sur crash)
journalctl --list-boots
journalctl -u mon-service -b -1 # avant-dernier boot
# Filtrer par priorité
journalctl -u mon-service -p err # erreurs seulement
journalctl -u mon-service -p warning..err
# Depuis une heure précise
journalctl -u mon-service --since "2026-05-13 06:00:00"
journalctl -u mon-service --since "1 hour ago"
# JSON pour parser avec jq
journalctl -u mon-service -o json | jq '.MESSAGE'
# Les logs du kernel autour d'un crash
journalctl -k --since "2026-05-13 06:00:00"
Les champs structurés de journald
journald stocke les logs avec des métadonnées structurées. Vous pouvez filtrer sur n'importe quel champ :
# Logs d'un PID spécifique
journalctl _PID=1234
# Logs d'un utilisateur
journalctl _UID=1000
# Logs d'un binaire
journalctl _EXE=/usr/bin/node
# Combiner les filtres
journalctl _SYSTEMD_UNIT=radar.service _PID=5678
# Voir tous les champs disponibles pour un message
journalctl -u radar.service -o verbose | head -50
systemd-analyze - comprendre ce qui se passe
# Temps de boot total
systemd-analyze
# Quels services prennent du temps
systemd-analyze blame
# Chemin critique du boot
systemd-analyze critical-chain
# Chemin critique d'un service spécifique
systemd-analyze critical-chain mon-service.service
# Vérifier la syntaxe d'une unité
systemd-analyze verify /etc/systemd/system/mon-service.service
# Générer un graphe SVG du boot
systemd-analyze plot > boot.svg
Les core dumps avec systemd-coredump
Quand un service segfault, systemd-coredump capture le core dump et le stocke dans le journal. Pas besoin de configurer ulimit -c ou un répertoire de core dump.
# Lister les core dumps
coredumpctl list
# Voir les infos d'un crash
coredumpctl info
# Lancer gdb sur le core dump
coredumpctl gdb mon-service
# Extraire le core dump pour analyse externe
coredumpctl dump mon-service -o /tmp/core.dump
Comprendre pourquoi un service ne démarre pas
# Voir l'état détaillé
systemctl status mon-service -l
# Voir les dépendances qui bloquent
systemctl list-dependencies mon-service
# Vérifier que toutes les dépendances sont satisfaites
systemctl is-active postgresql.service
systemctl is-active network-online.target
# Simuler le démarrage sans vraiment démarrer
systemd-run --unit=test-run --service-type=exec /opt/app/bin/server
Les instances - un service pour plusieurs configs
Le problème : vous avez plusieurs instances d'un même service (plusieurs sites nginx, plusieurs workers, plusieurs environnements) et vous dupliquez les unit files en changeant juste le nom. C'est du copier-coller et ça finit toujours par diverger.
Les unités template résolvent ça avec le caractère @ :
[Unit]
Description=Worker instance %i
After=network-online.target
[Service]
Type=exec
User=worker
ExecStart=/opt/worker/bin/worker --config /etc/worker/%i.conf
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
%i est remplacé par l'instance dans le nom de l'unité. %I est la version URL-décodée de %i.
# Démarrer des instances
systemctl start worker@production.service
systemctl start worker@staging.service
systemctl start worker@eu-west.service
# Activer au démarrage
systemctl enable worker@production.service
# Voir toutes les instances
systemctl list-units 'worker@*'
# Logs d'une instance
journalctl -u worker@production.service
Les specifiers disponibles
%i # nom de l'instance (après le @)
%I # nom de l'instance URL-décodé
%n # nom complet de l'unité
%p # préfixe (avant le @)
%H # hostname
%m # machine ID
%u # nom de l'utilisateur
%h # home directory de l'utilisateur
systemd-run - lancer des processus one-shot proprement
Le remplacement de nohup command &, screen -dm command, et autres hacks pour lancer un processus en arrière-plan sans terminal.
# Lancer un job avec logs dans le journal
systemd-run --unit=mon-job /opt/script/migration.sh
# Suivre les logs
journalctl -u mon-job -f
# Avec des contraintes de ressources
systemd-run --unit=gros-job \
--property=CPUQuota=50% \
--property=MemoryMax=2G \
/opt/script/traitement-lourd.sh
# Lancer dans un scope utilisateur (sans root)
systemd-run --user --unit=mon-job-user /opt/script/script.sh
# Avec un timer one-shot (dans 10 minutes)
systemd-run --on-active=10min /opt/script/rappel.sh
# Lancer et attendre la fin
systemd-run --wait /opt/script/migration.sh
echo "Migration terminée avec code: $?"
Pourquoi pas nohup ?
nohup command & : pas de logs intégrés, pas de gestion des ressources, le processus orphelin continue même si le parent crashe, vous ne savez pas si c'est encore en train de tourner sans faire un ps aux | grep.
systemd-run : logs dans journald, gestion des ressources via cgroups, systemctl status pour voir l'état, systemctl stop pour l'arrêter proprement. C'est la même chose mais géré.
journald - au-delà de journalctl -f
La rotation et la taille
Par défaut journald stocke les logs en mémoire (/run/log/journal/) et sur disque (/var/log/journal/) si le répertoire existe. La taille est limitée à 10% de la partition par défaut.
# /etc/systemd/journald.conf
[Journal]
Storage=persistent # forcer le stockage sur disque
Compress=yes # compresser les logs anciens
SystemMaxUse=2G # taille max sur disque
SystemKeepFree=500M # espace libre minimum à garder
SystemMaxFileSize=100M # taille max par fichier de journal
MaxRetentionSec=3month # durée de rétention
MaxFileSec=1week # rotation hebdomadaire
# Voir l'utilisation actuelle
journalctl --disk-usage
# Nettoyer manuellement
journalctl --vacuum-size=1G # garder max 1G
journalctl --vacuum-time=1month # garder max 1 mois
Exporter vers syslog ou Elasticsearch
# Forwarder vers syslog (pour rsyslog ou syslog-ng)
# /etc/systemd/journald.conf
ForwardToSyslog=yes
# Exporter en JSON pour Elasticsearch ou Loki
journalctl -o json -f | \
curl -s -X POST "http://elasticsearch:9200/logs/_bulk" \
-H "Content-Type: application/json" \
--data-binary @-
Les logs structurés depuis votre application
Votre application peut écrire des logs structurés que journald indexe :
# Depuis bash
echo "MESSAGE=Déploiement terminé
DEPLOYMENT_ID=d-7f3a2b1c
ENV=prod
SEVERITY=INFO" | systemd-cat -t radar
# Filtrer sur un champ custom
journalctl DEPLOYMENT_ID=d-7f3a2b1c
Les priorités
journalctl -p 0 # emerg
journalctl -p 1 # alert
journalctl -p 2 # crit
journalctl -p 3 # err
journalctl -p 4 # warning
journalctl -p 5 # notice
journalctl -p 6 # info
journalctl -p 7 # debug
# Plage de priorités
journalctl -p warning..err # warning et err seulement
journalctl -p err # err et au-dessus (emerg, alert, crit, err)
Les targets et le boot
Comprendre le graphe de dépendances
# Voir les dépendances d'un service
systemctl list-dependencies mon-service
# Voir ce qui dépend d'un service
systemctl list-dependencies --reverse mon-service
# Graphe complet
systemctl list-dependencies --all multi-user.target
Optimiser le boot
# Temps total
systemd-analyze
# Les coupables
systemd-analyze blame | head -20
# Le chemin critique
systemd-analyze critical-chain
# Désactiver les services inutiles
systemctl disable bluetooth.service
systemctl disable cups.service
systemctl mask ModemManager.service # mask empêche même le démarrage manuel
La différence entre disable et mask : disable retire le service du démarrage automatique mais il peut encore être démarré manuellement. mask crée un lien symbolique vers /dev/null - le service ne peut être démarré par aucun moyen tant qu'il est masqué.
Créer un target custom
Pour grouper des services qui doivent démarrer ensemble :
# /etc/systemd/system/app.target
[Unit]
Description=Application Stack
Requires=postgresql.service redis.service
After=postgresql.service redis.service
systemctl start app.target # démarre postgresql et redis
systemctl stop app.target # arrête les deux
À retenir
| Commande | Pour quoi |
|---|---|
systemctl edit mon-service | Modifier une unité sans toucher l'original |
systemctl list-timers --all | Voir tous les timers et leur prochaine exécution |
systemd-analyze calendar "expr" | Vérifier une expression calendrier |
systemd-analyze security mon-service | Score de sécurité d'une unité |
systemd-analyze blame | Quels services ralentissent le boot |
journalctl -u service -b -1 | Logs du boot précédent |
journalctl -u service -p err | Erreurs seulement |
journalctl --vacuum-size=1G | Nettoyer les vieux logs |
coredumpctl list | Lister les core dumps |
systemd-run --wait commande | Lancer un job et attendre la fin |
systemctl mask service | Empêcher tout démarrage |
FAQ
Quelle différence entre `Wants=` et `Requires=` ?
Wants= démarre la dépendance si possible mais continue même si elle échoue. Requires= stoppe votre service si la dépendance échoue ou s'arrête. Pour la plupart des dépendances réseau, Wants= est suffisant - votre service peut gérer l'absence de réseau. Pour une base de données sans laquelle votre service ne peut rien faire, Requires=.
`systemctl restart` vs `systemctl reload` ?
restart arrête et redémarre le processus. reload envoie un signal au processus pour qu'il recharge sa config sans redémarrer. reload est plus rapide et ne coupe pas les connexions existantes - préférez-le quand le service le supporte. Si le service ne supporte pas reload, il retourne une erreur.
Comment voir si un timer a bien tourné ?
systemctl list-timers --all montre la dernière exécution et la prochaine. journalctl -u mon-service.service montre les logs de la dernière exécution. Si le service n'apparaît pas dans les logs alors qu'il devrait avoir tourné, vérifiez Persistent=true dans le timer.
`Type=simple` vs `Type=exec` - lequel choisir ?
Type=exec dans tous les cas nouveaux. Type=simple est le défaut historique mais Type=exec est plus précis - systemd attend que le processus ait fait son exec() avant de considérer le service comme démarré. La différence est rare en pratique mais exec est plus correct.
Comment débugger un service qui démarre mais est marqué `failed` immédiatement ?
systemctl status service -l d'abord. Si le code de sortie est 0 mais le service est failed, vérifiez SuccessExitStatus= dans l'unité. Si le service quitte trop vite avec Type=simple, essayez Type=exec ou Type=oneshot selon le cas. journalctl -u service -b pour les logs complets du dernier boot.
Pour aller plus loin
- systemd.io - la documentation officielle, exhaustive
- Arch Linux wiki - systemd - la meilleure documentation pratique, même si vous n'utilisez pas Arch
man systemd.service,man systemd.timer,man systemd.exec- les pages de manuel sont bien écrites et couvrent tous les paramètres- systemd by example - des exemples concrets pour chaque cas d'usage