Aller au contenu

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.

/etc/systemd/system/monapp.service :

[Unit]
Description=Application monapp
After=network.target
[Service]
Type=simple
User=monapp
Group=monapp
WorkingDirectory=/srv/monapp/app
EnvironmentFile=/etc/monapp.env
ExecStart=/usr/bin/python3 /srv/monapp/app/serveur.py
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target

Préparer l’utilisateur et le fichier d’environnement :

Fenêtre de terminal
sudo useradd --system --create-home --home-dir /srv/monapp --shell /usr/sbin/nologin monapp
echo "MON_ENV=production" | sudo tee /etc/monapp.env
sudo chmod 600 /etc/monapp.env

Charger, activer, vérifier :

Fenêtre de terminal
systemd-analyze verify /etc/systemd/system/monapp.service # vérifie la syntaxe (aucune sortie = correct)
sudo systemctl daemon-reload
sudo systemctl enable --now monapp
systemctl status monapp
curl http://127.0.0.1:3000/
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)
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

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).

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=60
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=3

Au-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é).

[Service]
Environment=NODE_ENV=production PORT=3000
Environment="MESSAGE=Bonjour tout le monde"
EnvironmentFile=/etc/monapp.env
EnvironmentFile=-/etc/default/monapp
  • EnvironmentFile= lit un fichier NOM=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 avec systemctl show).

À 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=SIGTERM
TimeoutStopSec=30
Fenêtre de terminal
sudo systemctl stop monapp
journalctl -u monapp -n 5 --no-pager # message d'arrêt propre de l'application
Fenêtre de terminal
sudo systemctl edit monapp
[Service]
Environment=PORT=3001
MemoryMax=300M

Le fichier est enregistré dans /etc/systemd/system/monapp.service.d/override.conf. Voir le résultat final :

Fenêtre de terminal
systemctl cat monapp # unité d'origine + surcharges

Pour vider une directive à liste (ExecStart) avant d’en définir une nouvelle : la répéter vide (ExecStart=) puis la redéfinir.

[Unit]
Description=Application Node.js
After=network.target
[Service]
User=monapp
WorkingDirectory=/srv/monapp/app
Environment=NODE_ENV=production
EnvironmentFile=/etc/monapp.env
ExecStart=/usr/bin/node src/serveur.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

Voir Node.js : mise en production.

Voir SSH : tunnels : ExecStart=/usr/bin/ssh -NT -o ExitOnForwardFailure=yes … avec Restart=always.

[Unit]
Description=Sauvegarde quotidienne
[Service]
Type=oneshot
ExecStart=/usr/local/bin/sauvegarde.sh
User=root

Dé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 »
Fenêtre de terminal
sudo systemd-run --unit=essai --property=MemoryMax=100M /usr/bin/python3 -m http.server 8000
sudo systemd-run --uid=monapp --pty /bin/bash # shell dans un contexte contrôlé
systemd-run --scope -p MemoryMax=500M ./commande # applique des limites à une commande
sudo systemctl stop essai
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.