◀ Retour au blog
Linux

Créer des services systemd

Publié le 25 Sep 2024· 7 min de lecture
#Linux#Systemd#Services

Gérer vos applications avec systemd

systemd est le gestionnaire de services standard sous Linux. Créer un service personnalisé permet de gérer le cycle de vie de vos applications : démarrage automatique au boot, redémarrage en cas de plantage, ordre de démarrage par rapport à la base de données, centralisation des logs et limitation des ressources. Tout cela sans superviseur supplémentaire à installer : systemd est déjà présent sur Debian, Ubuntu, RHEL et la quasi-totalité des distributions actuelles.

Pour une application PHP, les cas d'usage typiques sont les workers de file de messages, les consommateurs de longue durée, les petits serveurs HTTP intégrés et les tâches planifiées. Voyons comment écrire des unités fiables, puis comment les exploiter au quotidien.

Anatomie d'un fichier unit

Un service est décrit par un fichier texte au format INI, placé dans /etc/systemd/system/ pour les unités propres à la machine. Il comporte trois sections :

  • [Unit] : description et dépendances vis-à-vis des autres unités.
  • [Service] : comment lancer, arrêter et relancer le processus, et sous quelle identité.
  • [Install] : à quelle cible rattacher le service lorsqu'on l'active avec systemctl enable.

Créer un service

# /etc/systemd/system/myapp.service
[Unit]
Description=Mon Application PHP
After=network.target mysql.service
Requires=mysql.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php bin/console messenger:consume async --limit=100
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Les directives importantes :

  • After et Requires : deux notions distinctes. After définit l'ordre de démarrage ; Requires définit une dépendance : si MySQL est arrêté, le service l'est aussi. Wants est une variante plus souple, qui démarre la dépendance sans échouer si elle est absente. Sur certaines distributions, le service s'appelle mariadb.service : vérifiez le nom exact.
  • Type=simple : le processus lancé par ExecStart est le processus principal et reste au premier plan. C'est le cas de messenger:consume. Utilisez Type=oneshot pour une commande qui s'exécute puis se termine.
  • User et Group : ne faites jamais tourner une application en root. Ici, le worker a les mêmes droits que PHP-FPM.
  • WorkingDirectory : le chemin de l'exécutable doit être absolu, mais les arguments comme bin/console sont résolus depuis ce répertoire.
  • Restart=always et RestartSec=5 : avec --limit=100, le worker s'arrête volontairement après 100 messages ; systemd le relance cinq secondes plus tard avec une mémoire propre. Restart=on-failure ne relancerait que les arrêts en erreur.
  • StandardOutput=journal : la sortie du processus est capturée par journald, horodatée et associée au service.

Commandes essentielles

sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
journalctl -u myapp -f

daemon-reload est obligatoire après chaque création ou modification d'un fichier unit : sans lui, systemd continue d'utiliser l'ancienne version. enable crée le lien qui démarre le service au boot, start le lance immédiatement ; enable --now fait les deux. Avant de charger un fichier, systemd-analyze verify /etc/systemd/system/myapp.service signale les directives inconnues et les fautes de frappe.

Pour les logs, journalctl offre des filtres précieux lors d'un incident :

# Logs depuis une heure, erreurs uniquement
journalctl -u myapp --since "1 hour ago" -p err

# Logs du démarrage courant, sans pagination
journalctl -u myapp -b --no-pager

Éviter les boucles de redémarrage

Un service qui plante immédiatement au démarrage (fichier de configuration manquant, base de données injoignable) sera relancé en boucle. Par défaut, systemd abandonne après 5 démarrages en 10 secondes et marque le service en échec. Avec un RestartSec=5, ce seuil n'est jamais atteint : ajustez donc la fenêtre dans la section [Unit] :

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=10

Ici, plus de 10 démarrages en 5 minutes font passer le service en état failed, ce qui est visible dans systemctl --failed et facile à surveiller. Une fois la cause corrigée, systemctl reset-failed myapp remet le compteur à zéro.

Gestion des workers Symfony Messenger

Pour les applications Symfony utilisant Messenger, créez un service avec plusieurs instances :

# /etc/systemd/system/[email protected]
[Unit]
Description=Symfony Messenger worker %i

[Service]
User=www-data
Group=www-data
ExecStart=/usr/bin/php /var/www/app/bin/console messenger:consume async --time-limit=3600
Restart=always

[Install]
WantedBy=multi-user.target

# Lancer 3 workers
sudo systemctl enable messenger-worker@{1..3}
sudo systemctl start messenger-worker@{1..3}

Le @ dans le nom du fichier en fait un template : messenger-worker@1, messenger-worker@2, etc. sont des instances distinctes du même fichier, et %i est remplacé par l'identifiant de l'instance. La notation {1..3} est une expansion du shell Bash, pas une fonctionnalité de systemd.

L'option --time-limit=3600 fait redémarrer chaque worker toutes les heures, ce qui limite l'effet des fuites mémoire. Vous pouvez aussi utiliser --memory-limit=128M. Lors d'un déploiement, les workers gardent l'ancien code en mémoire : exécutez php bin/console messenger:stop-workers à la fin du déploiement. Chaque worker termine son message en cours puis s'arrête, et systemd le relance sur le nouveau code.

Variables d'environnement et surcharges

Ne mettez pas de secrets dans le fichier unit, qui est lisible par tous. Utilisez un fichier d'environnement protégé :

[Service]
EnvironmentFile=/etc/myapp/env
Environment=APP_ENV=prod

Le fichier /etc/myapp/env contient des lignes CLE=valeur et doit appartenir à root avec des droits 600 : systemd le lit avant de changer d'utilisateur. Pour modifier un service fourni par un paquet sans toucher au fichier d'origine, utilisez sudo systemctl edit nginx : la commande crée un fichier de surcharge dans /etc/systemd/system/nginx.service.d/, qui survit aux mises à jour.

Limiter les ressources et isoler le service

systemd place chaque service dans son propre cgroup, ce qui permet de plafonner ses ressources et de restreindre ce qu'il peut voir du système :

[Service]
MemoryMax=512M
CPUQuota=50%
TasksMax=100
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

MemoryMax tue le processus s'il dépasse la limite, au lieu de laisser le noyau choisir une victime au hasard. CPUQuota=50% limite le service à la moitié d'un cœur. PrivateTmp lui donne un /tmp isolé, ProtectSystem=full rend /usr, /boot et /etc en lecture seule, et ProtectHome masque les répertoires personnels. La commande systemd-analyze security myapp attribue une note d'exposition au service et liste les options de durcissement disponibles.

Remplacer cron par un timer

Les timers systemd sont une alternative moderne à cron : les exécutions apparaissent dans le journal, un échec est visible dans systemctl --failed, et une exécution manquée pendant un arrêt de la machine peut être rattrapée. Un timer active un service du même nom :

# /etc/systemd/system/app-cleanup.service
[Unit]
Description=Nettoyage quotidien de l'application

[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php bin/console app:cleanup

# /etc/systemd/system/app-cleanup.timer
[Unit]
Description=Lance app-cleanup tous les jours à 3h

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now app-cleanup.timer
systemctl list-timers
systemd-analyze calendar "*-*-* 03:00:00"

Persistent=true déclenche le service au démarrage si l'exécution prévue a été manquée. systemd-analyze calendar vérifie une expression et affiche la prochaine échéance.

Bonnes pratiques

  • Définir les dépendances avec After et Requires
  • Configurer Restart=always pour la résilience
  • Utiliser les journaux systemd plutôt que des fichiers de log
  • Limiter les ressources avec les cgroups
  • Ne jamais exécuter une application en root : toujours User et Group
  • Stocker les secrets dans un EnvironmentFile protégé
  • Utiliser systemctl edit pour surcharger un service fourni par un paquet
  • Relancer les workers à chaque déploiement avec messenger:stop-workers

systemd n'est pas la bonne réponse partout : dans un environnement conteneurisé, c'est l'orchestrateur (Docker, Kubernetes) qui gère le cycle de vie des processus. Mais sur un serveur classique ou un VPS, quelques fichiers unit bien écrits remplacent avantageusement Supervisor, les scripts d'init maison et une bonne partie de la crontab.