Guide

systemd tout ce que vous faites encore avec cron, screen et des scripts bash

systemd gère bien plus que vos services. Timers pour remplacer cron, sandboxing, journald, unités templates et systemd-run pour les jobs one-shot.

Sommaire

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.
  • .mount et .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 @ :

worker@.service

[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

CommandePour quoi
systemctl edit mon-serviceModifier une unité sans toucher l'original
systemctl list-timers --allVoir tous les timers et leur prochaine exécution
systemd-analyze calendar "expr"Vérifier une expression calendrier
systemd-analyze security mon-serviceScore de sécurité d'une unité
systemd-analyze blameQuels services ralentissent le boot
journalctl -u service -b -1Logs du boot précédent
journalctl -u service -p errErreurs seulement
journalctl --vacuum-size=1GNettoyer les vieux logs
coredumpctl listLister les core dumps
systemd-run --wait commandeLancer un job et attendre la fin
systemctl mask serviceEmpê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