ITSkillsCenter
Blog

systemd : créer et superviser un service Linux — tutoriel pas-à-pas

10 min de lecture

📍 Article principal du programme : Linux fondamentaux 2026 — commandes, services, dépannage
Ce tutoriel fait partie du programme « Linux fondamentaux ». Pour la vue d’ensemble, lisez d’abord le guide principal.

Introduction

Sur tout Linux moderne — Ubuntu 24.04 et 26.04 LTS, Debian 13, AlmaLinux 9, Rocky Linux 9, Fedora, openSUSE — c’est systemd qui démarre le système, supervise les services, monte les disques, gère les logs et orchestre l’extinction. Apprendre à créer et superviser un service systemd est l’une des compétences les plus rentables d’un administrateur ou d’un développeur qui déploie sur un VPS : en cinq lignes de configuration, vous transformez un script Node ou Python fragile en démon résilient qui redémarre tout seul, journalise proprement, et tient au reboot. Ce tutoriel vous fait passer de zéro à un service personnalisé prêt pour la production, en huit étapes appliquées.

Prérequis

  • Un Linux récent avec systemd 245+ : Ubuntu 24.04/26.04, Debian 13, AlmaLinux/Rocky 9. La version 260 publiée en mars 2026 retire le support des scripts SysV historiques mais reste rétrocompatible avec la syntaxe utilisée ici.
  • Un compte avec accès sudo.
  • Les bases de la ligne de commande et des permissions Linux.
  • Niveau attendu : intermédiaire débutant.
  • Temps estimé : 90 minutes.

Étape 1 — Vérifier l’installation et lire l’état général

Avant de toucher à quoi que ce soit, prenez une photo de l’état du système. Cette discipline est la même que celle d’un médecin : on ausculte avant de prescrire. Les commandes suivantes ne modifient rien et donnent un panorama complet en moins de trente secondes.

systemctl --version                        # version exacte de systemd
systemctl status                           # vue arborescente du PID 1
systemctl list-units --type=service        # tous les services chargés
systemctl list-units --state=failed        # uniquement les services en échec
systemctl list-unit-files --type=service   # services connus, activés ou non

La sortie de systemctl status montre un arbre où le PID 1 est systemd lui-même, suivi de toutes les unités actives. Sur un serveur sain, systemctl list-units --state=failed renvoie « 0 loaded units listed ». Si vous voyez des services en rouge, notez-les — ce sera votre premier exercice de dépannage. La distinction entre list-units (services chargés en mémoire) et list-unit-files (services dont le fichier existe sur disque) est utile : un service peut exister sans être actif, et inversement un service peut tourner sans avoir été activé au démarrage.

Étape 2 — Maîtriser systemctl sur un service existant

Avant de créer votre propre service, entraînez-vous sur un service déjà installé — Nginx, par exemple, ou n’importe quel démon disponible. Les six verbes essentiels couvrent 95% des opérations quotidiennes.

sudo systemctl start nginx          # démarre maintenant
sudo systemctl stop nginx           # arrête maintenant
sudo systemctl restart nginx        # arrête puis démarre
sudo systemctl reload nginx         # recharge la config sans interruption (si supporté)
sudo systemctl enable nginx         # activera au prochain démarrage
sudo systemctl disable nginx        # désactivera au prochain démarrage
sudo systemctl enable --now nginx   # active ET démarre dans la foulée
systemctl status nginx              # état détaillé : actif/inactif, PID, RAM, dernières lignes de log

La distinction entre restart et reload mérite d’être bien comprise. restart tue le processus et le relance — il y a une coupure de quelques millisecondes à quelques secondes pendant laquelle le service ne répond plus. reload envoie un signal SIGHUP au démon qui relit sa configuration sans terminer ses connexions actives — il n’y a aucune coupure, mais tous les démons ne supportent pas cette opération. Nginx, sshd, postfix la supportent ; un script Node sans gestion de SIGHUP non. La sortie de systemctl status nginx affiche les dix dernières lignes de log du service — c’est souvent suffisant pour diagnostiquer un échec de démarrage.

Étape 3 — Anatomie d’un fichier d’unité

Un service systemd est décrit par un fichier texte au format INI, traditionnellement placé dans /etc/systemd/system/ pour les services personnalisés et dans /lib/systemd/system/ pour ceux fournis par les paquets. Le fichier comporte trois sections obligatoires : [Unit] qui décrit le service et ses dépendances, [Service] qui dit comment le lancer, et [Install] qui précise quand l’activer.

[Unit]
Description=Mon API Node.js
Documentation=https://exemple.com/docs
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=mon-app
Group=mon-app
WorkingDirectory=/opt/mon-app
ExecStart=/usr/bin/node /opt/mon-app/server.js
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Ce fichier minimal illustre l’essentiel. After= et Wants= garantissent que le réseau est disponible avant le lancement. Type=simple indique que le processus pointé par ExecStart est le service lui-même (par opposition à Type=forking pour les vieux démons qui se détachent). User= et Group= exécutent le service sous un compte non privilégié, première barrière de sécurité. Restart=on-failure redémarre automatiquement en cas de crash, RestartSec=5s attend cinq secondes avant la relance pour éviter les boucles de crash. StandardOutput=journal envoie la sortie standard dans le journal systemd, lisible avec journalctl.

Étape 4 — Créer votre premier service

Mettons les concepts en pratique avec un service Node minimal. Cette séquence reproduit ce que vous ferez à chaque déploiement d’API maison sur un VPS.

# 1. Créer un utilisateur de service dédié
sudo useradd -r -s /usr/sbin/nologin -d /opt/mon-app -m mon-app

# 2. Déposer un script Node trivial
sudo tee /opt/mon-app/server.js >/dev/null <<'JS'
const http = require('http');
const port = process.env.PORT || 3000;
http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Bonjour depuis systemd\n');
}).listen(port, () => console.log('listening on', port));
JS

# 3. Ajuster les droits
sudo chown -R mon-app:mon-app /opt/mon-app
sudo chmod 750 /opt/mon-app

# 4. Créer le fichier d'unité
sudo tee /etc/systemd/system/mon-app.service >/dev/null <<'UNIT'
[Unit]
Description=Mon API Node de test
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=mon-app
Group=mon-app
WorkingDirectory=/opt/mon-app
Environment=PORT=3000
ExecStart=/usr/bin/node /opt/mon-app/server.js
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
UNIT

# 5. Recharger systemd, activer et démarrer
sudo systemctl daemon-reload
sudo systemctl enable --now mon-app

# 6. Vérifier
systemctl status mon-app
curl http://127.0.0.1:3000/

La commande systemctl daemon-reload est indispensable après toute modification d'un fichier d'unité — sans elle, systemd continue à utiliser l'ancienne version. Si systemctl status mon-app affiche « active (running) » et que curl retourne « Bonjour depuis systemd », le service tourne. Pour vérifier la résilience, tuez le processus avec sudo pkill -f server.js puis relancez systemctl status mon-app cinq secondes plus tard : il sera revenu, redémarré automatiquement par systemd. C'est ce comportement qui transforme un script fragile en démon de production.

Étape 5 — Exposer la configuration et les variables d'environnement

Une bonne pratique consiste à séparer le code (dans /opt/mon-app/) des variables d'environnement (dans /etc/mon-app/). systemd offre EnvironmentFile= pour charger un fichier .env sans avoir à modifier l'unité à chaque changement.

sudo mkdir -p /etc/mon-app
sudo tee /etc/mon-app/env >/dev/null <<'ENV'
PORT=3000
DATABASE_URL=postgres://user:pass@localhost/db
LOG_LEVEL=info
ENV
sudo chmod 640 /etc/mon-app/env
sudo chown root:mon-app /etc/mon-app/env

Ensuite, dans le fichier d'unité, remplacez la ligne Environment=PORT=3000 par EnvironmentFile=/etc/mon-app/env et faites sudo systemctl daemon-reload && sudo systemctl restart mon-app. Le service charge alors les variables depuis le fichier, et vous pouvez modifier la configuration sans toucher à l'unité. Les permissions 640 avec propriétaire root et groupe mon-app garantissent que seul le service y accède en lecture, jamais en écriture, et qu'aucun autre utilisateur du système ne peut lire les secrets.

Étape 6 — Durcir avec les directives de sandboxing

systemd offre plus de quarante directives qui restreignent ce qu'un service peut faire au système. Bien utilisées, elles limitent considérablement l'impact d'une compromission applicative. Quelques-unes méritent d'être posées par défaut sur tout service exposé.

[Service]
NoNewPrivileges=yes              # impossible d'élever les privilèges via setuid
ProtectSystem=strict             # / en lecture seule, sauf /etc /var
ProtectHome=yes                  # /home, /root, /run/user invisibles
PrivateTmp=yes                   # /tmp et /var/tmp privés au service
PrivateDevices=yes               # accès aux périphériques refusé
ProtectKernelTunables=yes        # /proc/sys et /sys en lecture seule
ProtectKernelModules=yes         # impossible de charger des modules noyau
RestrictAddressFamilies=AF_INET AF_INET6  # uniquement IPv4 et IPv6, pas AF_UNIX ni AF_NETLINK
LockPersonality=yes              # interdit personality(2)
MemoryDenyWriteExecute=yes       # interdit pages mémoire write+exec (anti-JIT exploit)

Toutes ces directives ne peuvent pas s'appliquer à n'importe quel service : MemoryDenyWriteExecute=yes casse certains runtimes JIT comme l'ancienne V8 ou les compilateurs JIT Java. Le bon réflexe est d'activer une directive à la fois, redémarrer, vérifier que le service tourne toujours, et continuer. La commande systemd-analyze security mon-app.service note la sécurité du service de 0 à 10 et liste les directives à ajouter pour l'améliorer — un excellent guide d'audit progressif.

Étape 7 — Drop-ins et minuteurs

Plutôt que de modifier un fichier d'unité fourni par un paquet (ce qui sera écrasé à la prochaine mise à jour), systemd permet de créer un drop-in, un fichier de surcharge qui ajoute ou remplace des directives sans toucher l'original.

sudo systemctl edit mon-app
# ouvre un éditeur ; on y met par exemple :
# [Service]
# MemoryMax=512M
# CPUQuota=50%

Le fichier créé est /etc/systemd/system/mon-app.service.d/override.conf. MemoryMax impose une limite mémoire au-delà de laquelle le service est tué par le mécanisme cgroups ; CPUQuota=50% plafonne l'usage CPU à un demi-cœur. Ces limites évitent qu'un bug applicatif (fuite mémoire, boucle infinie) sature un VPS modeste et entraîne tout le système.

Pour les tâches récurrentes, systemd remplace cron par les timers. Un timer est une unité .timer associée à une unité .service qui s'exécute selon une planification. La syntaxe OnCalendar=*-*-* 03:00:00 reproduit l'équivalent d'un cron quotidien à 3 heures du matin, avec en bonus l'option Persistent=true qui rattrape les exécutions manquées en cas d'indisponibilité du serveur. Le sujet est traité en détail dans les ressources cron/timer existantes du programme.

Étape 8 — Vérification et nettoyage

Pour valider votre maîtrise, exécutez la séquence suivante qui audite le service créé puis nettoie tout proprement.

systemctl status mon-app                    # actif ?
journalctl -u mon-app -n 30 --no-pager      # 30 dernières lignes de log
systemd-analyze security mon-app.service    # note de sécurité
systemctl is-enabled mon-app                # enabled ?
systemctl show mon-app -p MainPID,User,Restart  # propriétés clés

# Nettoyage si exercice terminé
sudo systemctl disable --now mon-app
sudo rm /etc/systemd/system/mon-app.service
sudo rm -rf /etc/mon-app /opt/mon-app
sudo userdel mon-app
sudo systemctl daemon-reload
sudo systemctl reset-failed mon-app 2>/dev/null || true

Si la note de sécurité est inférieure à 5, ajoutez les directives de sandboxing vues à l'étape 6 et observez la note remonter. Une note autour de 1 indique un service quasi-isolé du reste du système ; la cible 2-3 est réaliste pour la plupart des applis Node ou Python sans configuration exotique. Cette discipline progressive est plus efficace qu'une tentative de tout durcir d'un coup, qui finit toujours par casser le service au pire moment.

Erreurs fréquentes

Erreur Cause Solution
« Failed to start » au boot Dépendance manquante ou réseau pas prêt Vérifier After=, ajouter Wants=network-online.target
Service redémarre en boucle Crash applicatif ; Restart=on-failure relance journalctl -u service -n 100, corriger le bug avant de redéployer
« Modification du fichier ignorée » Oubli de daemon-reload sudo systemctl daemon-reload && sudo systemctl restart service
« Permission denied » dans le service User= n'a pas accès au répertoire/fichier chown -R user:group /opt/... ou ajuster bits
OOM Killer tue le service Fuite mémoire ou limite trop basse journalctl -k | grep -i oom, ajuster MemoryMax
Variables d'environnement absentes Confusion entre Environment= et fichier .env applicatif Toujours utiliser EnvironmentFile= pour les configs externes
Type=forking mais service vu comme mort PID file mal configuré Préférer Type=simple ou Type=notify pour les services modernes

Tutoriels associés

Lectures complémentaires

FAQ

Pourquoi systemd plutôt que cron pour les tâches récurrentes ?
Trois avantages concrets : la journalisation structurée dans le journal système (visible avec journalctl -u service.service), la possibilité de définir des dépendances entre tâches, et l'option Persistent=true qui rattrape les exécutions ratées si le serveur était éteint. cron reste plus simple à écrire à la volée mais perd vite en lisibilité au-delà de quelques tâches.

Quelle différence entre Type=simple et Type=notify ?
Avec Type=simple, systemd considère le service comme actif dès que le binaire est lancé, sans attendre qu'il soit prêt à accepter des connexions. Avec Type=notify, le service envoie explicitement un signal à systemd quand il est prêt (via la bibliothèque sd_notify ou la variable d'environnement NOTIFY_SOCKET). Le second est préférable pour les services lents à initialiser, parce qu'il permet à systemd de retarder le lancement des services dépendants.

Comment limiter la mémoire et le CPU sans tuer le service ?
MemoryMax est un plafond dur : si le service le dépasse, le noyau le tue. MemoryHigh est plus souple : au-delà, le noyau ralentit le service au lieu de le tuer. CPUWeight=100 fixe une priorité relative entre services concurrents (par défaut 100 ; un service à 200 obtient le double de CPU de ceux à 100 en cas de contention). Ces directives s'appuient sur cgroups v2, activé par défaut sur tous les Linux récents.

Comment voir tous les services qui plantent le plus souvent ?
journalctl --list-boots liste les démarrages successifs ; journalctl _SYSTEMD_UNIT=mon-app.service | grep -i "main process exited" liste les sorties anormales. Pour une vue agrégée, l'outil systemd-cgtop affiche en temps réel l'usage CPU/mémoire par service, et systemd-cgls donne l'arbre des cgroups.

Faut-il utiliser supervisord, PM2 ou systemd ?
Sur un serveur Linux moderne, systemd est la bonne réponse par défaut : il est déjà installé, il s'intègre au journal système, il survit au reboot, et il offre des directives de sandboxing impossibles à reproduire avec un superviseur applicatif. PM2 reste utile pour le développement local et certains workflows Node très spécifiques, mais pour la production, le moins de couches superflues est le meilleur choix.

Comment migrer d'un script SysV existant ?
systemd 260 a retiré le support des scripts SysV historiques. Pour migrer, écrivez un fichier .service équivalent : la directive ExecStart= remplace le bloc start), ExecStop= remplace stop), et les dépendances chkconfig deviennent des After= et Requires=. La plupart des conversions tiennent en quinze lignes — moins que le script original.

Comment exécuter une commande après un échec sans redémarrer en boucle ?
La directive OnFailure= permet de déclencher une autre unité quand le service échoue. Typiquement, on définit un service auxiliaire alerte@.service qui envoie un courriel ou un message Slack avec le nom de l'unité en argument. La construction OnFailure=alerte@%n.service chaîne les deux proprement : à chaque échec, l'alerte part automatiquement, ce qui évite d'avoir à surveiller manuellement les journalctl --state=failed. Combiné avec StartLimitBurst=3 et StartLimitIntervalSec=60, on obtient une politique « trois tentatives en une minute, puis abandon et alerte » qui protège contre les boucles de crash sans masquer les échecs durables.

Quels sont les bons réflexes pour observer un service en production ?
Trois habitudes valent d'être prises dès le premier déploiement. Premièrement, lire le journal en flux tendu pendant les premiers redémarrages avec journalctl -u mon-app -f dans une session tmux dédiée — c'est là qu'on voit immédiatement les warnings qu'un déploiement à l'aveugle masque. Deuxièmement, activer la collecte des métriques cgroups via un agent comme node_exporter (Prometheus) ou Telegraf pour suivre la mémoire, le CPU et les redémarrages dans le temps. Troisièmement, documenter dans un runbook les commandes de diagnostic récurrentes (« où sont les logs », « comment redémarrer », « comment lire l'état ») pour que n'importe quel coéquipier puisse intervenir sans appeler son auteur à 2 h du matin.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité