
L’API Container est le cœur de Dagger. Elle permet de manipuler des conteneurs comme des objets : charger une image, exécuter des commandes, configurer l’environnement, puis récupérer les résultats. Ce guide couvre toutes les méthodes essentielles pour construire des pipelines CI/CD robustes.
Dans ce guide, vous apprendrez à :
- Charger des images avec
from_()et comprendre le système de cache - Exécuter des commandes avec
with_exec()(simple et chaîné) - Configurer les variables d’environnement avec
with_env_variable() - Définir le contexte d’exécution (
with_workdir(),with_user()) - Récupérer les sorties (
stdout(),stderr(),exit_code()) - Appliquer le pattern chaînage fluent pour des pipelines lisibles
Charger une image avec from_()
Section intitulée « Charger une image avec from_() »La méthode from_() charge une image Docker de base depuis un registre (Docker Hub par défaut).
Syntaxe de base
Section intitulée « Syntaxe de base »container = dag.container().from_("python:3.12-slim")Exemples avec différentes images
Section intitulée « Exemples avec différentes images »@functionasync def image_info(self, image: str = "python:3.12-slim") -> str: """Charge une image et affiche ses informations.""" return await ( dag.container() .from_(image) .with_exec(["cat", "/etc/os-release"]) .stdout() )Tester la fonction :
dagger call image-info --image="alpine:latest"Sortie attendue :
NAME="Alpine Linux"ID=alpineVERSION_ID=3.21.3...Interpolation de tags
Section intitulée « Interpolation de tags »Vous pouvez construire dynamiquement le tag de l’image :
@functionasync def python_version(self, version: str = "3.12") -> str: """Vérifie la version Python dans un conteneur.""" image = f"python:{version}-slim" return await ( dag.container() .from_(image) .with_exec(["python", "--version"]) .stdout() )dagger call python-version --version=3.11# Sortie : Python 3.11.14Registres privés
Section intitulée « Registres privés »Pour un registre privé avec authentification :
container = ( dag.container() .with_registry_auth("registry.example.com", "username", dag.secret("REGISTRY_PASSWORD")) .from_("registry.example.com/myapp:latest"))Exécuter des commandes avec with_exec()
Section intitulée « Exécuter des commandes avec with_exec() »La méthode with_exec() exécute une commande dans le conteneur. Elle prend une liste d’arguments (pas une chaîne).
Syntaxe de base
Section intitulée « Syntaxe de base »container = ( dag.container() .from_("alpine:latest") .with_exec(["echo", "Hello, Dagger!"]))Chaîner plusieurs commandes
Section intitulée « Chaîner plusieurs commandes »Chaque with_exec() ajoute une couche au conteneur. Les commandes s’exécutent séquentiellement :
@functionasync def multi_exec(self) -> str: """Installe curl et affiche sa version.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["apk", "add", "--no-cache", "curl"]) .with_exec(["curl", "--version"]) .stdout() )Avantage du chaînage : chaque étape est cachée séparément. Si vous modifiez la dernière commande, les précédentes ne sont pas réexécutées.
Commandes shell avec pipes
Section intitulée « Commandes shell avec pipes »Pour les pipes, redirections ou substitutions, utilisez sh -c :
@functionasync def shell_command(self) -> str: """Compte les lignes avec un pipe.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["sh", "-c", "echo 'ligne1\nligne2\nligne3' | wc -l"]) .stdout() )dagger call shell-command# Sortie : 3Variables d’environnement avec with_env_variable()
Section intitulée « Variables d’environnement avec with_env_variable() »Définir une variable
Section intitulée « Définir une variable »@functionasync def env_simple(self, name: str = "Dagger") -> str: """Définit et utilise une variable d'environnement.""" return await ( dag.container() .from_("alpine:latest") .with_env_variable("GREETING", f"Hello, {name}!") .with_exec(["sh", "-c", "echo $GREETING"]) .stdout() )dagger call env-simple --name="DevOps"# Sortie : Hello, DevOps!Chaîner plusieurs variables
Section intitulée « Chaîner plusieurs variables »Vous pouvez définir autant de variables que nécessaire en chaînant les appels. Chaque with_env_variable() crée une nouvelle couche avec la variable définie. Les variables sont accessibles par toutes les commandes suivantes dans la chaîne.
@functionasync def env_multiple(self) -> str: """Définit plusieurs variables d'environnement.""" return await ( dag.container() .from_("alpine:latest") .with_env_variable("APP_NAME", "my-app") .with_env_variable("APP_ENV", "development") .with_env_variable("APP_DEBUG", "true") .with_exec(["sh", "-c", "echo $APP_NAME - $APP_ENV debug=$APP_DEBUG"]) .stdout() )Expansion de variables
Section intitulée « Expansion de variables »Parfois, vous voulez construire une variable à partir d’autres variables existantes. Le paramètre expand=True active l’expansion des variables dans la valeur, similaire à ce que fait le shell. Sans ce paramètre, la chaîne ${BASE_PATH} serait interprétée littéralement.
@functionasync def env_expand(self) -> str: """Utilise l'expansion de variables.""" return await ( dag.container() .from_("alpine:latest") .with_env_variable("BASE_PATH", "/app") .with_env_variable("FULL_PATH", "${BASE_PATH}/config", expand=True) .with_exec(["sh", "-c", "echo $FULL_PATH"]) .stdout() )dagger call env-expandContexte d’exécution : workdir et user
Section intitulée « Contexte d’exécution : workdir et user »Par défaut, les commandes s’exécutent à la racine du conteneur (/) avec l’utilisateur root. Dans un vrai pipeline, vous voulez souvent travailler dans un répertoire dédié (/app) avec un utilisateur non-root pour des raisons de sécurité. Dagger offre deux méthodes pour configurer ce contexte.
Définir le répertoire de travail
Section intitulée « Définir le répertoire de travail »La méthode with_workdir() définit le répertoire courant pour les commandes suivantes. C’est l’équivalent du WORKDIR dans un Dockerfile ou du cd dans un script shell. Toutes les commandes chaînées après with_workdir() s’exécutent depuis ce répertoire.
@functionasync def workdir_demo(self) -> str: """Définit le répertoire de travail.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["mkdir", "-p", "/app/src"]) .with_workdir("/app/src") .with_exec(["pwd"]) .stdout() )dagger call workdir-demoChanger l’utilisateur d’exécution
Section intitulée « Changer l’utilisateur d’exécution »La méthode with_user() change l’utilisateur qui exécute les commandes suivantes. C’est l’équivalent de USER dans un Dockerfile. Cette pratique est recommandée pour la sécurité : ne pas exécuter vos applications en tant que root limite l’impact d’une éventuelle compromission.
@functionasync def user_demo(self) -> str: """Change l'utilisateur d'exécution.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["adduser", "-D", "myuser"]) .with_user("myuser") .with_exec(["whoami"]) .stdout() )dagger call user-demo# Sortie : myuserCombiner workdir, user et env
Section intitulée « Combiner workdir, user et env »Dans un pipeline réaliste, vous combinez généralement les trois : un utilisateur dédié, un répertoire de travail propre et des variables d’environnement pour la configuration. L’ordre des appels est important : créez d’abord l’utilisateur, puis les répertoires avec les bonnes permissions, et enfin configurez le contexte d’exécution.
@functionasync def context_full(self) -> str: """Combine tous les contextes.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["adduser", "-D", "appuser"]) .with_exec(["mkdir", "-p", "/home/appuser/app"]) .with_exec(["chown", "-R", "appuser:appuser", "/home/appuser/app"]) .with_user("appuser") .with_workdir("/home/appuser/app") .with_env_variable("APP_HOME", "/home/appuser/app") .with_exec(["sh", "-c", "echo User: $(whoami), Dir: $(pwd), Home: $APP_HOME"]) .stdout() )Récupérer les sorties
Section intitulée « Récupérer les sorties »Après avoir exécuté des commandes, vous devez récupérer leurs résultats. Dagger sépare proprement la sortie standard (stdout), la sortie d’erreur (stderr) et le code de retour (exit_code). Cette séparation facilite le parsing des résultats et la détection des erreurs.
stdout() : sortie standard
Section intitulée « stdout() : sortie standard »La méthode stdout() récupère tout ce que la commande a écrit sur sa sortie standard. C’est la méthode la plus utilisée pour obtenir le résultat d’une commande. Elle retourne une chaîne de caractères (souvent avec un \n final).
result = await container.with_exec(["echo", "Hello"]).stdout()# result = "Hello\n"stderr() : sortie d’erreur
Section intitulée « stderr() : sortie d’erreur »La méthode stderr() récupère la sortie d’erreur de la commande. Les messages d’erreur, warnings et logs de debug sont souvent écrits sur stderr. Cette méthode est utile pour analyser pourquoi une commande a échoué ou pour capturer les messages de diagnostic.
@functionasync def output_stderr(self) -> str: """Récupère la sortie d'erreur.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["sh", "-c", "echo 'Erreur simulée' >&2"]) .stderr() )exit_code() : code de sortie
Section intitulée « exit_code() : code de sortie »Par défaut, with_exec() échoue si le code de sortie est non-zéro. Pour capturer le code sans échouer, utilisez expect=dagger.ReturnType.ANY :
import dagger
@functionasync def output_exit_code(self) -> int: """Récupère le code de sortie.""" return await ( dag.container() .from_("alpine:latest") .with_exec(["sh", "-c", "exit 42"], expect=dagger.ReturnType.ANY) .exit_code() )dagger call output-exit-code# Sortie : 42Pattern chaînage fluent (builder)
Section intitulée « Pattern chaînage fluent (builder) »Le chaînage fluent (ou pattern builder) est une technique de programmation où chaque méthode retourne l’objet modifié, permettant d’enchaîner les appels sur une seule ligne. En Dagger, ce pattern est fondamental : vous construisez progressivement votre conteneur en chaînant les configurations, puis vous l’exécutez avec une méthode terminale.
Ce pattern offre deux avantages majeurs : la lisibilité (chaque ligne décrit une action) et la réutilisabilité (vous pouvez créer des bases partagées entre plusieurs fonctions).
Créer une base réutilisable
Section intitulée « Créer une base réutilisable »Plutôt que de dupliquer la configuration de base dans chaque fonction, extrayez-la dans une fonction dédiée. Cette fonction retourne un Container (sans await) que d’autres fonctions peuvent étendre. C’est particulièrement utile quand plusieurs pipelines partagent la même image et les mêmes variables.
@functiondef base_python(self, version: str = "3.12") -> dagger.Container: """Crée un conteneur Python de base réutilisable.""" return ( dag.container() .from_(f"python:{version}-slim") .with_env_variable("PYTHONDONTWRITEBYTECODE", "1") .with_env_variable("PYTHONUNBUFFERED", "1") .with_workdir("/app") )Étendre la base
Section intitulée « Étendre la base »Une fois la base définie, vous pouvez l’étendre dans d’autres fonctions. Appelez simplement la méthode de base et continuez la chaîne. Chaque fonction peut ajouter ses propres commandes tout en bénéficiant de la configuration partagée.
@functionasync def build_chain(self, version: str = "3.12") -> str: """Construit un conteneur par étapes chaînées.""" return await ( self.base_python(version) .with_exec(["pip", "install", "--no-cache-dir", "requests"]) .with_exec(["python", "-c", "import requests; print(requests.__version__)"]) .stdout() )dagger call build-chain --version=3.12# Sortie : 2.32.5Avantages du pattern builder
Section intitulée « Avantages du pattern builder »Regroups ici les principaux bénéfices de cette approche comparée à l’écriture de scripts linéaires ou de configurations YAML :
| Aspect | Bénéfice |
|---|---|
| Réutilisation | La base est définie une fois, utilisée partout |
| Lisibilité | Chaque méthode décrit une action claire |
| Cache | Chaque étape est cachée séparément |
| Testabilité | Chaque fonction est testable indépendamment |
| Typage | L’IDE propose l’autocomplétion |
TP : Pipeline lint Python avec ruff
Section intitulée « TP : Pipeline lint Python avec ruff »Mettons en pratique tout ce que nous avons vu avec un pipeline de lint complet. Dans ce TP, vous allez créer une fonction qui analyse du code Python avec ruff, un linter ultra-rapide écrit en Rust. Ce cas d’usage réel illustre comment combiner images, commandes, montages de fichiers et récupération des sorties.
Structure du projet
Section intitulée « Structure du projet »Le projet suit la structure standard d’un module Dagger Python. Le code source à analyser se trouve dans src/, et le pipeline Dagger dans .dagger/ :
Répertoirecontainer-api/
- pyproject.toml
Répertoiresrc/
Répertoirehello_container/
- init .py
- bad_code.py
Répertoire.dagger/
Répertoiresrc/
Répertoirecontainer_api/
- main.py
Fonction lint_check
Section intitulée « Fonction lint_check »Cette fonction monte un répertoire source dans le conteneur et exécute ruff pour vérifier le code. Notez l’utilisation de with_mounted_directory() pour passer les fichiers locaux au conteneur, et de expect=dagger.ReturnType.ANY pour ne pas échouer si ruff trouve des erreurs de style.
@functionasync def lint_check(self, source: dagger.Directory) -> str: """Vérifie le code Python avec ruff.""" return await ( dag.container() .from_("python:3.12-slim") .with_exec(["pip", "install", "--no-cache-dir", "ruff"]) .with_mounted_directory("/app", source) .with_workdir("/app") .with_exec(["ruff", "check", "."], expect=dagger.ReturnType.ANY) .stdout() )Exécuter le lint
Section intitulée « Exécuter le lint »Depuis la racine du projet, appelez la fonction en lui passant le répertoire source. Dagger monte automatiquement le contenu de ./src dans le conteneur à l’emplacement /app.
dagger call lint-check --source=./srcSortie avec erreurs :
F401 [*] `os` imported but unused --> hello_container/bad_code.py:3:8
F401 [*] `sys` imported but unused --> hello_container/bad_code.py:4:8
F841 Local variable `x` is assigned to but never used --> hello_container/bad_code.py:13:5
Found 3 errors.Pipeline lint complet
Section intitulée « Pipeline lint complet »Un vrai pipeline de lint combine souvent plusieurs vérifications : analyse statique (erreurs, imports inutilisés) et formatage (style, indentation). Cette fonction montre comment réutiliser une base commune pour exécuter deux vérifications et combiner leurs résultats.
@functionasync def lint_full(self, source: dagger.Directory) -> str: """Pipeline lint complet: check + format.""" base = ( dag.container() .from_("python:3.12-slim") .with_exec(["pip", "install", "--no-cache-dir", "ruff"]) .with_mounted_directory("/app", source) .with_workdir("/app") )
# Check check_result = await ( base .with_exec(["ruff", "check", ".", "--output-format=text"], expect=dagger.ReturnType.ANY) .stdout() )
# Format check format_result = await ( base .with_exec(["ruff", "format", "--check", "."], expect=dagger.ReturnType.ANY) .stdout() )
return f"=== LINT CHECK ===\n{check_result}\n=== FORMAT CHECK ===\n{format_result}"Anti-patterns à éviter
Section intitulée « Anti-patterns à éviter »Certaines pratiques courantes nuisent aux performances, à la maintenabilité ou au debugging de vos pipelines. Voici les erreurs les plus fréquentes et comment les corriger.
1. Tout dans un seul with_exec
Section intitulée « 1. Tout dans un seul with_exec »Regrouper toutes les commandes dans un seul sh -c désactive le cache granulaire de Dagger. Si une seule commande change, tout est réexécuté. De plus, en cas d’échec, vous ne savez pas quelle commande a échoué.
# ❌ Mauvais cache, difficile à debugger.with_exec(["sh", "-c", "pip install ruff && ruff check . && ruff format ."])
# ✅ Chaque étape cachée séparément.with_exec(["pip", "install", "ruff"]).with_exec(["ruff", "check", "."]).with_exec(["ruff", "format", "--check", "."])2. Chemins hardcodés
Section intitulée « 2. Chemins hardcodés »Les chemins en dur comme /home/john/project rendent votre pipeline inutilisable par d’autres développeurs. Utilisez des paramètres avec des valeurs par défaut pour rester flexible.
# ❌ Non réutilisable.with_workdir("/home/john/project")
# ✅ Paramétrabledef my_function(self, workdir: str = "/app"): return dag.container().with_workdir(workdir)3. Oublier expect pour les commandes qui peuvent échouer
Section intitulée « 3. Oublier expect pour les commandes qui peuvent échouer »Par défaut, Dagger échoue si une commande retourne un code non-zéro. Pour les outils d’analyse (lint, tests), vous voulez souvent capturer le résultat même en cas d’erreur pour l’afficher à l’utilisateur.
# ❌ Échoue si ruff trouve des erreurs.with_exec(["ruff", "check", "."])
# ✅ Capture le résultat même si erreurs.with_exec(["ruff", "check", "."], expect=dagger.ReturnType.ANY)4. Mélanger sync et async
Section intitulée « 4. Mélanger sync et async »Oublier await sur une méthode asynchrone est une erreur subtile : le code s’exécute sans erreur mais retourne un objet coroutine au lieu du résultat réel. Utilisez toujours await avec stdout(), stderr(), sync() et exit_code().
# ❌ Oublier awaitresult = container.stdout() # Retourne un coroutine, pas le résultat
# ✅ Toujours await pour les méthodes terminalesresult = await container.stdout()Dépannage
Section intitulée « Dépannage »Les erreurs suivantes sont fréquentes lors de la prise en main de l’API Container. Ce tableau vous aide à identifier rapidement la cause et la solution.
| Problème | Cause probable | Solution |
|---|---|---|
TypeError: with_exec() argument must be a list | Chaîne passée au lieu d’une liste | Utiliser ["cmd", "arg1"] |
exit code: 1 inattendu | Commande échouée | Ajouter expect=dagger.ReturnType.ANY pour capturer l’erreur |
| Variable non définie dans shell | Pas de sh -c | Utiliser ["sh", "-c", "echo $VAR"] |
Permission denied | Mauvais utilisateur | Vérifier with_user() et permissions |
No such file or directory | workdir inexistant | Créer le répertoire avec with_exec(["mkdir", "-p", ...]) |
| Image non trouvée | Tag incorrect | Vérifier le format image:tag |
À retenir
Section intitulée « À retenir »from_()charge une image — le underscore évite le conflit avec le mot-clé Pythonwith_exec()prend une liste d’arguments, pas une chaîne- Chaînez plusieurs
with_exec()pour un meilleur cache sh -cest nécessaire pour les pipes et redirections shellwith_env_variable()avecexpand=Truepermet de référencer d’autres variablesstdout(),stderr(),exit_code()récupèrent les résultatsexpect=dagger.ReturnType.ANYpour ne pas échouer sur un code non-zéro- Pattern builder : retourner
Containerpour créer des bases réutilisables