systemd : écrire un service
Faire tourner une application comme service : démarrage automatique, redémarrage en cas de panne, journaux centralisés. Les exemples utilisent une application fictive (monapp) qui écoute sur le port 3000.
Un service minimal
Section intitulée « Un service minimal »/etc/systemd/system/monapp.service :
[Unit]Description=Application monappAfter=network.target
[Service]Type=simpleUser=monappGroup=monappWorkingDirectory=/srv/monapp/appEnvironmentFile=/etc/monapp.envExecStart=/usr/bin/python3 /srv/monapp/app/serveur.pyRestart=on-failureRestartSec=3
[Install]WantedBy=multi-user.targetPréparer l’utilisateur et le fichier d’environnement :
sudo useradd --system --create-home --home-dir /srv/monapp --shell /usr/sbin/nologin monappecho "MON_ENV=production" | sudo tee /etc/monapp.envsudo chmod 600 /etc/monapp.envCharger, activer, vérifier :
systemd-analyze verify /etc/systemd/system/monapp.service # vérifie la syntaxe (aucune sortie = correct)sudo systemctl daemon-reloadsudo systemctl enable --now monappsystemctl status monappcurl http://127.0.0.1:3000/Les sections
Section intitulée « Les sections »| Section | Rôle |
|---|---|
[Unit] |
Description et relations avec d’autres unités (voir Dépendances et cibles) |
[Service] |
Comment lancer le programme |
[Install] |
Comment l’activer (WantedBy=multi-user.target : démarrage au boot normal) |
Directives de [Service]
Section intitulée « Directives de [Service] »| Directive | Rôle |
|---|---|
Type=simple |
Le processus lancé est le service (par défaut) |
Type=forking |
Le programme se détache en arrière-plan (ancien style de démon) : indiquer PIDFile= |
Type=oneshot |
Exécute une tâche puis se termine (sauvegarde, script) |
Type=notify |
Le programme signale quand il est prêt (compatible sd_notify) |
User, Group |
Compte d’exécution (jamais root sans nécessité) |
WorkingDirectory |
Dossier de travail |
ExecStart |
Commande de démarrage |
ExecStartPre, ExecStartPost |
Commandes avant et après le démarrage |
ExecReload |
Commande de rechargement (/bin/kill -HUP $MAINPID) |
ExecStop |
Commande d’arrêt (sinon SIGTERM est envoyé) |
Restart |
Politique de redémarrage (voir plus bas) |
RestartSec |
Attente avant le redémarrage |
TimeoutStartSec, TimeoutStopSec |
Délais avant d’abandonner le démarrage ou l’arrêt |
Les chemins doivent être absolus
Section intitulée « Les chemins doivent être absolus »ExecStart n’utilise pas le PATH : indiquer le chemin complet des programmes (/usr/bin/node, command -v node pour le trouver). Les fonctions du shell (redirections, &&, *, variables $HOME) ne sont pas interprétées. Pour les utiliser :
ExecStart=/bin/sh -c 'exec /usr/bin/python3 /srv/monapp/app/serveur.py >> /srv/monapp/app.log 2>&1'exec fait que le programme remplace le shell et reçoit directement les signaux d’arrêt. La plupart du temps, il est préférable de laisser l’application écrire sur la sortie standard : systemd la capture dans le journal (voir Journal).
Politique de redémarrage
Section intitulée « Politique de redémarrage »Restart= |
Redémarre si… |
|---|---|
no (défaut) |
Jamais |
on-failure |
Code de sortie non nul, signal, délai dépassé (recommandé) |
on-abnormal |
Signal ou délai dépassé |
always |
Toujours, même après un arrêt normal |
on-success |
Seulement après une sortie réussie |
Limiter les boucles de redémarrage :
[Unit]StartLimitIntervalSec=60StartLimitBurst=5
[Service]Restart=on-failureRestartSec=3Au-delà de 5 démarrages en 60 secondes, le service passe en failed (résultat start-limit-hit) et systemd cesse d’essayer. Réinitialiser le compteur avec sudo systemctl reset-failed monapp, après avoir corrigé la cause.
Un redémarrage forcé testé : sudo kill -9 $(systemctl show -p MainPID --value monapp) fait relancer le service après RestartSec (compteur NRestarts incrémenté).
Variables d’environnement
Section intitulée « Variables d’environnement »[Service]Environment=NODE_ENV=production PORT=3000Environment="MESSAGE=Bonjour tout le monde"EnvironmentFile=/etc/monapp.envEnvironmentFile=-/etc/default/monappEnvironmentFile=lit un fichierNOM=valeur, une ligne par variable. Le préfixe-rend le fichier facultatif (pas d’erreur s’il n’existe pas).- Les secrets (mots de passe, jetons) se placent dans un fichier à droits
600, pas dans l’unité (lisible par tous avecsystemctl show).
Arrêt propre
Section intitulée « Arrêt propre »À l’arrêt, systemd envoie SIGTERM, attend TimeoutStopSec (90 s par défaut), puis SIGKILL. L’application doit intercepter SIGTERM pour terminer ses requêtes et quitter :
[Service]KillSignal=SIGTERMTimeoutStopSec=30sudo systemctl stop monappjournalctl -u monapp -n 5 --no-pager # message d'arrêt propre de l'applicationSurcharger sans modifier l’unité : drop-ins
Section intitulée « Surcharger sans modifier l’unité : drop-ins »sudo systemctl edit monapp[Service]Environment=PORT=3001MemoryMax=300MLe fichier est enregistré dans /etc/systemd/system/monapp.service.d/override.conf. Voir le résultat final :
systemctl cat monapp # unité d'origine + surchargesPour vider une directive à liste (ExecStart) avant d’en définir une nouvelle : la répéter vide (ExecStart=) puis la redéfinir.
Exemple : application Node.js
Section intitulée « Exemple : application Node.js »[Unit]Description=Application Node.jsAfter=network.target
[Service]User=monappWorkingDirectory=/srv/monapp/appEnvironment=NODE_ENV=productionEnvironmentFile=/etc/monapp.envExecStart=/usr/bin/node src/serveur.jsRestart=on-failureRestartSec=5
[Install]WantedBy=multi-user.targetVoir Node.js : mise en production.
Exemple : tunnel SSH permanent
Section intitulée « Exemple : tunnel SSH permanent »Voir SSH : tunnels : ExecStart=/usr/bin/ssh -NT -o ExitOnForwardFailure=yes … avec Restart=always.
Exemple : tâche ponctuelle (oneshot)
Section intitulée « Exemple : tâche ponctuelle (oneshot) »[Unit]Description=Sauvegarde quotidienne
[Service]Type=oneshotExecStart=/usr/local/bin/sauvegarde.shUser=rootDéclenchée par un timer ou à la main (sudo systemctl start sauvegarde.service).
Lancer une commande dans un service temporaire : systemd-run
Section intitulée « Lancer une commande dans un service temporaire : systemd-run »sudo systemd-run --unit=essai --property=MemoryMax=100M /usr/bin/python3 -m http.server 8000sudo systemd-run --uid=monapp --pty /bin/bash # shell dans un contexte contrôlésystemd-run --scope -p MemoryMax=500M ./commande # applique des limites à une commandesudo systemctl stop essaiDépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
status=203/EXEC |
Chemin de ExecStart inexistant ou non exécutable |
status=217/USER |
User= inconnu |
status=200/CHDIR |
WorkingDirectory inexistant |
| Le service redémarre en boucle | journalctl -u monapp -e ; StartLimitBurst atteint → reset-failed |
| Variables non prises en compte | Fichier absent, droits, ou la priorité de EnvironmentFile (voir plus haut) |
| Le programme se termine tout de suite | Type=simple avec un programme qui se détache : utiliser Type=forking, ou lancer en premier plan |
| Les modifications ne s’appliquent pas | sudo systemctl daemon-reload puis sudo systemctl restart |
Détails dans Dépannage.
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Les directives de cette page sont identiques sur systemd 255 et 259.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Manuel :
man systemd.service,man systemd.exec - Suite : Timers (planification)