
Quadlet transforme des fichiers déclaratifs en unités systemd pour vos conteneurs Podman. Vous décrivez l’intention (image, ports, volumes), systemd gère le cycle de vie (démarrage, restart, logs).
À la fin de ce cours, vous saurez :
- Démarrer un conteneur comme un service systemd
- Gérer logs, restart et dépendances
- Structurer un mini-stack (volume + réseau + conteneur)
- Choisir entre rootless et rootful
Ce que Quadlet résout
Section intitulée « Ce que Quadlet résout »Avant Quadlet, vous aviez deux options pour exécuter des conteneurs au démarrage :
-
podman generate systemd: génère une unité.serviceavec la commandepodman runcomplète. Problème : toute modification nécessite de régénérer le fichier. -
Écrire un
.serviceà la main : fragile, difficile à maintenir, pas de gestion native des volumes/réseaux.
Quadlet apporte une approche déclarative :
| Approche | Maintenabilité | Fonctionnalités |
|---|---|---|
| Script shell + cron | Fragile | Aucune |
.service manuel | Moyenne | Basiques |
podman generate systemd | Moyenne | Complètes mais figées |
| Quadlet | Excellente | Complètes et évolutives |
Quadlet est maintenant l’approche recommandée par Red Hat pour la production.
Prérequis
Section intitulée « Prérequis »- Linux avec systemd
- Podman 4.4+ (Quadlet intégré depuis cette version)
- Connaissances de base de systemd (
systemctl,journalctl)
Droit au but
Section intitulée « Droit au but »Modèle mental : comment Quadlet fonctionne
Section intitulée « Modèle mental : comment Quadlet fonctionne »Le générateur systemd
Section intitulée « Le générateur systemd »Quadlet est un générateur systemd. Il s’exécute automatiquement :
- Au boot du système
- Après chaque
systemctl daemon-reload
Il lit vos fichiers .container, .volume, .network et génère les unités .service correspondantes.
Où placer les fichiers
Section intitulée « Où placer les fichiers »~/.config/containers/systemd/├── nginx.container├── data.volume└── app-network.networkCommandes avec --user :
systemctl --user daemon-reloadsystemctl --user start nginxjournalctl --user -u nginx/etc/containers/systemd/├── nginx.container├── data.volume└── app-network.networkCommandes sans --user :
sudo systemctl daemon-reloadsudo systemctl start nginxsudo journalctl -u nginxTypes de fichiers Quadlet
Section intitulée « Types de fichiers Quadlet »Quadlet reconnait plusieurs types de fichiers, chacun correspondant à un type de ressource Podman. L’extension du fichier détermine ce que Quadlet va créer :
| Extension | Ce qu’il définit | Équivalent Podman |
|---|---|---|
.container | Un conteneur | podman run |
.volume | Un volume nommé | podman volume create |
.network | Un réseau | podman network create |
.pod | Un pod | podman pod create |
.image | Une image à pull | podman pull |
.kube | Un play kube | podman kube play |
.build | Un build d’image | podman build |
Dans la pratique, vous utiliserez principalement .container, .volume et .network. Les autres types couvrent des cas d’usage plus spécifiques comme l’intégration avec des manifests Kubernetes (.kube) ou le build d’images en CI/CD (.build).
Voir ce qui est généré
Section intitulée « Voir ce qui est généré »Pour inspecter l’unité systemd générée à partir de votre fichier Quadlet :
# Rootlesssystemctl --user cat nginx.service
# Rootfulsystemctl cat nginx.serviceDémarrage en 5 minutes
Section intitulée « Démarrage en 5 minutes »Créons un serveur nginx comme service systemd.
-
Créer le répertoire Quadlet (rootless)
Fenêtre de terminal mkdir -p ~/.config/containers/systemd -
Créer le fichier
nginx.containerFenêtre de terminal cat > ~/.config/containers/systemd/nginx.container << 'EOF'[Unit]Description=Serveur web Nginx[Container]Image=docker.io/library/nginx:alpinePublishPort=8080:80[Service]Restart=always[Install]WantedBy=default.targetEOF -
Recharger systemd
Fenêtre de terminal systemctl --user daemon-reload -
Démarrer le service
Fenêtre de terminal systemctl --user start nginx -
Vérifier
Fenêtre de terminal systemctl --user status nginxRésultat ● nginx.service - Serveur web NginxLoaded: loaded (/home/bob/.config/containers/systemd/nginx.container; generated)Active: active (running) since Thu 2026-02-13 10:30:00 CET; 5s agoMain PID: 12345 (conmon)
Testez avec curl http://localhost:8080 — vous devriez voir la page d’accueil nginx.
Activer au démarrage
Section intitulée « Activer au démarrage »systemctl --user enable nginxAnatomie d’un fichier Quadlet
Section intitulée « Anatomie d’un fichier Quadlet »Un fichier .container se compose de 4 sections :
[Unit]Description=Description du serviceAfter=network-online.target
[Container]Image=docker.io/library/nginx:alpinePublishPort=8080:80Volume=data:/var/www:ZEnvironment=MA_VAR=valeurReadOnly=true
[Service]Restart=alwaysTimeoutStartSec=300
[Install]WantedBy=default.targetSection [Unit]
Section intitulée « Section [Unit] »Cette section utilise la syntaxe standard systemd pour définir les métadonnées et les dépendances du service. Les directives les plus utiles :
| Directive | Usage |
|---|---|
Description= | Description affichée dans systemctl status |
After= | Démarre après ces unités |
Requires= | Dépendance forte (si l’autre échoue, celui-ci aussi) |
Wants= | Dépendance faible |
Astuce dépendances : utilisez Requires= pour les dépendances critiques (base de données pour une application) et Wants= pour les dépendances optionnelles (monitoring, logs). After= ne garantit que l’ordre de démarrage, pas que le service dépendant est fonctionnel.
Section [Container]
Section intitulée « Section [Container] »C’est le cœur de votre fichier Quadlet. Chaque directive correspond à une option de podman run, mais avec une syntaxe déclarative plus lisible :
| Directive Quadlet | Option podman run | Exemple |
|---|---|---|
Image= | (image) | nginx:alpine |
PublishPort= | -p | 8180:80 |
Volume= | -v | data:/app:Z |
Environment= | -e | DEBUG=true |
EnvironmentFile= | --env-file | /etc/myapp/env |
User= | --user | 1000:1000 |
ReadOnly= | --read-only | true |
AddCapability= | --cap-add | NET_BIND_SERVICE |
DropCapability= | --cap-drop | ALL |
SecurityLabelDisable= | --security-opt label=disable | true |
Network= | --network | app-network.network |
PodmanArgs= | (options supplémentaires) | --init --userns=keep-id |
Exec= | (commande) | /bin/sh -c "sleep infinity" |
AutoUpdate= | label auto-update | registry |
Deux directives essentielles :
PodmanArgs=: permet de passer des optionspodman runnon supportées directement par Quadlet. Utilisez-le pour--userns=keep-id(rootless) ou--security-opt.Network=: référence un fichier.networkdu même répertoire. Quadlet gère automatiquement l’ordre de création.
Section [Service]
Section intitulée « Section [Service] »Cette section contrôle le comportement systemd du service. Les paramètres les plus importants concernent la gestion des redémarrages et les timeouts :
| Directive | Usage | Valeur typique |
|---|---|---|
Restart= | Politique de restart | always, on-failure |
RestartSec= | Délai entre restarts | 5s |
TimeoutStartSec= | Timeout au démarrage | 300 (pour pull d’image) |
TimeoutStopSec= | Timeout à l’arrêt | 30 |
Pourquoi ces valeurs ?
Restart=always: en production, vous voulez que le service redémarre automatiquement après un crash ou un reboot.RestartSec=5s: évite les boucles de redémarrage rapide en cas de problème persistant.TimeoutStartSec=300: 5 minutes pour permettre le pull d’images lourdes (plusieurs Go).
Section [Install]
Section intitulée « Section [Install] »Définit quand le service doit démarrer :
[Install]WantedBy=default.target # rootless# ouWantedBy=multi-user.target # rootfulRecette 1 — Service durci (production)
Section intitulée « Recette 1 — Service durci (production) »Voici un service nginx prêt pour la production, avec les bonnes pratiques de sécurité :
[Unit]Description=Nginx productionAfter=network-online.target
[Container]Image=docker.io/library/nginx:alpinePublishPort=8080:80
# Sécurité : filesystem read-only + tmpfs pour les dossiers d'écritureReadOnly=trueTmpfs=/tmpTmpfs=/var/cache/nginxTmpfs=/var/run
# Sécurité : capabilities minimalesDropCapability=ALLAddCapability=NET_BIND_SERVICEAddCapability=CHOWNAddCapability=SETGIDAddCapability=SETUID
# Options supplémentaires via PodmanArgsPodmanArgs=--security-opt no-new-privileges --init
# Volume pour les données (lecture seule)Volume=web-content:/usr/share/nginx/html:ro,Z
[Service]Restart=alwaysRestartSec=5sTimeoutStartSec=300TimeoutStopSec=30
[Install]WantedBy=default.targetVérifications :
# Recharger et démarrersystemctl --user daemon-reloadsystemctl --user start nginx-prod
# Vérifier le statutsystemctl --user status nginx-prod
# Voir l'unité généréesystemctl --user cat nginx-prod.service
# Tester le restart automatiquepodman kill $(podman ps -q --filter name=systemd-nginx-prod)sleep 10systemctl --user status nginx-prod # Doit être "active (running)"Recette 2 — Rootless propre
Section intitulée « Recette 2 — Rootless propre »En mode rootless, quelques points d’attention :
Répertoire des fichiers
Section intitulée « Répertoire des fichiers »# Créer le répertoiremkdir -p ~/.config/containers/systemd
# Vos fichiers doivent être icils ~/.config/containers/systemd/Ports exposés
Section intitulée « Ports exposés »En rootless, vous ne pouvez pas exposer de ports < 1024 sans configuration spéciale :
# ❌ Ne fonctionne pas en rootlessPublishPort=80:80
# ✅ Port > 1024PublishPort=8080:80Volumes et permissions
Section intitulée « Volumes et permissions »Problème classique : le conteneur n’a pas accès aux fichiers de l’hôte.
Solution 1 : utiliser PodmanArgs=--userns=keep-id
[Container]Image=nginx:alpineVolume=./html:/usr/share/nginx/html:ZPodmanArgs=--userns=keep-idSolution 2 : utiliser le suffixe :U (chown automatique)
[Container]Volume=myvolume:/data:UQuand basculer en rootful
Section intitulée « Quand basculer en rootful »Le mode rootless est le choix par défaut pour la sécurité (pas de privilèges root). Cependant, certaines situations nécessitent le mode rootful :
| Situation | Rootless | Rootful |
|---|---|---|
| Dev local | ✅ | |
| Service utilisateur | ✅ | |
| Port < 1024 nécessaire | ✅ | |
| Accès hardware (GPU, USB) | ✅ | |
| Performance réseau critique | ✅ | |
| Service système partagé | ✅ |
Règle de décision : commencez toujours en rootless. Basculez en rootful uniquement si vous avez une contrainte technique (port 80/443, GPU) ou organisationnelle (service partagé entre utilisateurs). Le passage en rootful se fait en plaçant vos fichiers dans /etc/containers/systemd/ au lieu de ~/.config/containers/systemd/.
Recette 3 — Stack multi-services
Section intitulée « Recette 3 — Stack multi-services »Créons une stack complète : PostgreSQL + application web, avec volume et réseau dédiés.
Structure des fichiers
Section intitulée « Structure des fichiers »Répertoire~/.config/containers/systemd/
- app-network.network
- pgdata.volume
- postgres.container
- webapp.container
1. Créer le réseau
Section intitulée « 1. Créer le réseau »[Unit]Description=Réseau applicatif
[Network]Subnet=10.89.0.0/24Gateway=10.89.0.12. Créer le volume
Section intitulée « 2. Créer le volume »[Unit]Description=Données PostgreSQL
[Volume]# Options par défaut suffisent3. Créer le service PostgreSQL
Section intitulée « 3. Créer le service PostgreSQL »[Unit]Description=Base de données PostgreSQLAfter=app-network.network pgdata.volume
[Container]Image=docker.io/library/postgres:16-alpineNetwork=app-network.networkVolume=pgdata.volume:/var/lib/postgresql/data:Z
# Variables d'environnementEnvironment=POSTGRES_USER=appEnvironment=POSTGRES_PASSWORD=secretEnvironment=POSTGRES_DB=myapp
# HealthcheckHealthCmd=pg_isready -U app -d myappHealthInterval=10sHealthTimeout=5sHealthRetries=3
[Service]Restart=alwaysTimeoutStartSec=300
[Install]WantedBy=default.target4. Créer le service application
Section intitulée « 4. Créer le service application »[Unit]Description=Application webAfter=postgres.containerRequires=postgres.container
[Container]Image=docker.io/myapp:latestNetwork=app-network.networkPublishPort=8080:3000
Environment=DATABASE_URL=postgres://app:secret@systemd-postgres:5432/myapp
PodmanArgs=--init --userns=keep-id
[Service]Restart=alwaysRestartSec=5s
[Install]WantedBy=default.targetDéployer la stack
Section intitulée « Déployer la stack »# Rechargersystemctl --user daemon-reload
# Démarrer (l'ordre est géré par les dépendances)systemctl --user start webapp
# Vérifiersystemctl --user status app-network pgdata postgres webappPoints clés
Section intitulée « Points clés »Cette stack illustre plusieurs mécanismes Quadlet importants :
| Directive | Ce qu’elle fait |
|---|---|
After=postgres.container | Démarre après PostgreSQL |
Requires=postgres.container | Si PostgreSQL échoue, webapp s’arrête |
Network=app-network.network | Utilise le réseau Quadlet |
Volume=pgdata.volume:/path | Utilise le volume Quadlet |
Comment Quadlet gère les ressources : quand vous référencez app-network.network dans Network=, Quadlet sait que c’est un fichier du même répertoire. Il crée le réseau systemd-app-network avant de démarrer le conteneur. Même logique pour les volumes.
Attention : les fichiers .network et .volume ne créent pas de services systemd visibles avec systemctl status. Ils créent les ressources Podman sous-jacentes (vérifiez avec podman network ls et podman volume ls).
Bonnes pratiques
Section intitulée « Bonnes pratiques »Un bon nommage facilite le debug et la maintenance à long terme :
- Fichiers : noms explicites (
postgres.container, pasdb.container). Quand vous aurez 10 services, vous regretterez les noms génériques. - Descriptions : rédigez-les pour
systemctl status. “Base de données PostgreSQL 16 - production” est plus utile que “db”. - Un service = un fichier : Quadlet gère les dépendances entre fichiers. Évitez de tout mettre dans un seul fichier complexe.
Restart et timeouts
Section intitulée « Restart et timeouts »[Service]# Toujours redémarrer en productionRestart=always
# Délai entre les redémarrages (évite les boucles)RestartSec=5s
# Timeout pour le pull d'image (images lourdes)TimeoutStartSec=300
# Timeout pour l'arrêt propreTimeoutStopSec=30Ordre de démarrage
Section intitulée « Ordre de démarrage »[Unit]# Dépendance forte : si postgres échoue, webapp ne démarre pasRequires=postgres.container
# Ordre : webapp démarre APRÈS postgresAfter=postgres.container
# Dépendance faible : si le réseau n'existe pas, on essaie quand mêmeWants=app-network.networkSécurité
Section intitulée « Sécurité »[Container]# Filesystem en lecture seuleReadOnly=trueTmpfs=/tmp
# Capabilities minimalesDropCapability=ALLAddCapability=NET_BIND_SERVICE
# Bloquer l'escalade de privilègesPodmanArgs=--security-opt no-new-privileges --initObservabilité
Section intitulée « Observabilité »L’un des grands avantages de Quadlet : vos conteneurs bénéficient automatiquement de l’infrastructure de logs systemd. Plus besoin de podman logs — utilisez journalctl :
# Statut rapide (conteneur actif ? PID ? mémoire ?)systemctl --user status webapp
# Logs en temps réel (équivalent de podman logs -f)journalctl --user -u webapp -f
# Logs depuis le dernier boot (utile après un redémarrage)journalctl --user -u webapp -b
# Logs des 10 dernières minutes (debug incident récent)journalctl --user -u webapp --since "10 minutes ago"Avantage journalctl : les logs persistent après l’arrêt du conteneur et sont automatiquement rotatés par systemd.
Dépannage
Section intitulée « Dépannage »”Je pose le fichier mais rien n’apparaît”
Section intitulée « ”Je pose le fichier mais rien n’apparaît” »-
Vérifiez le répertoire
Fenêtre de terminal # Rootlessls ~/.config/containers/systemd/# Rootfulls /etc/containers/systemd/ -
Vérifiez l’extension
Le fichier doit se terminer par
.container,.volume,.network, etc. -
Rechargez systemd
Fenêtre de terminal systemctl --user daemon-reload -
Vérifiez que l’unité existe
Fenêtre de terminal systemctl --user list-unit-files | grep monservice
”Le service démarre puis s’arrête”
Section intitulée « ”Le service démarre puis s’arrête” »Cause 1 : image inexistante
# Vérifier les logsjournalctl --user -u monservice -n 50
# Si l'image n'existe pas, la pull manuellementpodman pull docker.io/library/nginx:alpineCause 2 : port déjà utilisé
# Vérifier quel processus utilise le portss -tlnp | grep 8080Cause 3 : problème de permissions
# Logs détaillésjournalctl --user -u monservice --no-pager
# Chercher "permission denied"”Rootless + volumes = permission denied”
Section intitulée « ”Rootless + volumes = permission denied” »Solution 1 : ajouter --userns=keep-id
[Container]PodmanArgs=--userns=keep-idVolume=./data:/app/data:ZSolution 2 : utiliser le suffixe :U
[Container]Volume=myvolume:/app/data:USolution 3 : sur SELinux, ajouter :Z
[Container]Volume=./data:/app/data:Z”Le conteneur ne se connecte pas à la base de données”
Section intitulée « ”Le conteneur ne se connecte pas à la base de données” »Vérifiez le nommage : Quadlet préfixe les conteneurs avec systemd-
# ❌ IncorrectEnvironment=DATABASE_HOST=postgres
# ✅ CorrectEnvironment=DATABASE_HOST=systemd-postgresCommandes de debug
Section intitulée « Commandes de debug »# Voir l'unité généréesystemctl --user cat monservice.service
# Tester la génération Quadlet manuellement/usr/libexec/podman/quadlet --dryrun --user
# Voir les logs systemd pour Quadletjournalctl --user -u systemd-generator -bTemplates prêts à copier
Section intitulée « Templates prêts à copier »Ces templates couvrent les cas d’usage courants. Copiez-les, adaptez Image= et PublishPort=, et vous avez un service fonctionnel.
Template conteneur simple
Section intitulée « Template conteneur simple »Le minimum viable pour démarrer rapidement. Adapté au développement local :
[Unit]Description=Mon service
[Container]Image=docker.io/library/nginx:alpinePublishPort=8080:80
[Service]Restart=always
[Install]WantedBy=default.targetTemplate conteneur durci
Section intitulée « Template conteneur durci »Pour la production : filesystem read-only, capabilities minimales, pas d’escalade de privilèges. Utilisez ce template comme base pour tout service exposé au réseau :
[Unit]Description=Service durci
[Container]Image=docker.io/library/nginx:alpinePublishPort=8080:80
ReadOnly=trueTmpfs=/tmpTmpfs=/var/run
DropCapability=ALLAddCapability=NET_BIND_SERVICE
PodmanArgs=--security-opt no-new-privileges --init --userns=keep-id
[Service]Restart=alwaysRestartSec=5sTimeoutStartSec=300
[Install]WantedBy=default.targetTemplate avec volume et réseau
Section intitulée « Template avec volume et réseau »Pour les services stateful (bases de données, caches). Ce template nécessite des fichiers .network et .volume complémentaires dans le même répertoire :
[Unit]Description=Service avec données persistantesAfter=mynetwork.network mydata.volume
[Container]Image=docker.io/library/postgres:16-alpineNetwork=mynetwork.networkVolume=mydata.volume:/var/lib/postgresql/data:Z
Environment=POSTGRES_PASSWORD=secret
HealthCmd=pg_isreadyHealthInterval=30s
[Service]Restart=alwaysTimeoutStartSec=300
[Install]WantedBy=default.targetÀ retenir
Section intitulée « À retenir »- Quadlet = fichiers déclaratifs (
.container,.volume,.network) → systemd gère le cycle de vie - Répertoires :
~/.config/containers/systemd/(rootless) ou/etc/containers/systemd/(rootful) - Workflow : créer fichier →
daemon-reload→start→status - Dépendances :
After=pour l’ordre,Requires=pour la criticité - Nommage : Quadlet préfixe avec
systemd-(important pour les connexions réseau) - Debug :
systemctl catpour voir l’unité générée,journalctlpour les logs