Introduction : Le Problème du Vendor Lock-in en CI/CD
Si vous avez déjà migré un projet de Jenkins vers GitLab CI, de GitLab CI vers GitHub Actions, ou de CircleCI vers n'importe quoi d'autre, vous connaissez la douleur. Chaque plateforme de CI/CD possède sa propre syntaxe, son propre système de plugins, ses propres abstractions. Vos pipelines, qui représentent parfois des semaines voire des mois de travail d'ingénierie, deviennent du code jetable à chaque migration.
Ce problème de vendor lock-in en CI/CD est celui que Dagger ambitionne de résoudre. Et le projet n'est pas porté par n'importe qui : son fondateur est Solomon Hykes, le créateur de Docker. L'homme qui a révolutionné le déploiement d'applications avec les conteneurs s'attaque désormais à la CI/CD avec la même philosophie : standardiser et rendre portable.
Architecture en un coup d'œil

Lancé en 2022 et ayant atteint sa maturité en 2025-2026, Dagger propose une approche radicalement différente : écrire vos pipelines CI/CD dans un vrai langage de programmation (Go, Python, TypeScript, PHP), les exécuter dans des conteneurs via un moteur basé sur BuildKit, et les faire tourner partout — localement sur votre machine, dans GitHub Actions, GitLab CI, Jenkins, CircleCI, ou n'importe quel environnement disposant d'un runtime de conteneurs.
Qu'est-ce que Dagger ?
Dagger est un moteur de CI/CD programmable et portable. Plutôt que d'écrire des fichiers YAML déclaratifs spécifiques à une plateforme, vous écrivez vos pipelines dans un langage de programmation standard, en utilisant un SDK Dagger.
Les Principes Fondamentaux
- Portabilité : Un pipeline Dagger fonctionne identiquement partout — en local, en CI, sur n'importe quel cloud
- Programmabilité : Utilisez un vrai langage (Go, Python, TypeScript, PHP) avec autocomplétion, typage, tests unitaires, et toutes les bibliothèques de l'écosystème
- Reproductibilité : Chaque étape s'exécute dans un conteneur, garantissant un environnement identique partout
- Caching intelligent : Dagger sait exactement ce qui a changé et ne ré-exécute que le nécessaire
- Composabilité : Les modules Dagger (Daggerverse) sont réutilisables et partageables
L'Équipe Derrière Dagger
Solomon Hykes, après avoir quitté Docker Inc. en 2018, a fondé Dagger avec une vision claire : appliquer les leçons de Docker à la CI/CD. L'équipe comprend également d'anciens ingénieurs Docker, des contributeurs Moby/BuildKit, et des vétérans de l'écosystème cloud-native. Le projet a levé plus de 40 millions de dollars en financement et est open-source sous licence Apache 2.0.
Architecture de Dagger
L'architecture de Dagger se compose de trois couches distinctes :
1. Le Dagger Engine
Le cœur de Dagger est son moteur d'exécution, qui tourne comme un conteneur Docker. Il est construit sur BuildKit (le même moteur qui construit les images Docker) et offre :
- Exécution conteneurisée : Chaque opération s'exécute dans un conteneur isolé
- Graph d'exécution (DAG) : Les opérations sont organisées en un graphe acyclique dirigé, permettant la parallélisation automatique
- Caching par contenu : Basé sur le content-addressable storage de BuildKit
- Networking : Gestion des services (bases de données de test, serveurs mock, etc.)
2. L'API GraphQL
Le Dagger Engine expose une API GraphQL que les SDKs utilisent pour communiquer. Cette architecture permet :
- L'ajout facile de nouveaux SDKs dans n'importe quel langage
- L'introspection complète des capacités du moteur
- La composition de modules écrits dans différents langages
- Le debugging via des outils GraphQL standard
3. Les SDKs
Les SDKs sont les bibliothèques que vous utilisez dans votre code pour interagir avec le Dagger Engine. Chaque SDK fournit une API typée et idiomatique pour son langage :
| SDK | Langage | Statut | Gestionnaire de paquets |
|---|---|---|---|
| dagger-go-sdk | Go | Stable | go modules |
| dagger-python-sdk | Python | Stable | pip / uv |
| dagger-typescript-sdk | TypeScript | Stable | npm / bun |
| dagger-php-sdk | PHP | Expérimental | composer |
Installation
Prérequis
- Docker Engine (ou un runtime compatible OCI) installé et fonctionnel
- Le langage de votre choix (Go 1.21+, Python 3.10+, Node.js 18+, ou PHP 8.2+)
Installer le CLI Dagger
# Installation sur Linux/macOS
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
# Ou via Homebrew (macOS/Linux)
brew install dagger/tap/dagger
# Vérifier l'installation
dagger version
# dagger v0.15.x (linux/amd64)
# Le Dagger Engine sera automatiquement téléchargé et démarré
# comme conteneur Docker lors de la première utilisationInitialiser un Projet Dagger
# Se placer dans le répertoire de votre projet
cd /chemin/vers/mon-projet
# Initialiser un module Dagger en Python
dagger init --sdk=python --name=ci
# Cela crée :
# dagger.json - Configuration du module
# dagger/ - Répertoire du module
# dagger/src/main.py - Point d'entrée du pipelinePremier Pipeline avec le SDK Python
Commençons par un pipeline concret et réaliste : construire, tester et publier une application Python.
Structure du Projet
mon-projet/
├── src/
│ ├── __init__.py
│ ├── app.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── test_app.py
│ └── test_utils.py
├── Dockerfile
├── pyproject.toml
├── requirements.txt
├── dagger.json
└── dagger/
└── src/
└── main.py # Notre pipeline DaggerLe Pipeline Complet en Python
# dagger/src/main.py
"""Pipeline CI/CD complet avec Dagger SDK Python."""
import dagger
from dagger import dag, function, object_type
@object_type
class Ci:
"""Module CI/CD pour notre application Python."""
@function
async def lint(self, source: dagger.Directory) -> str:
"""Exécute le linting avec ruff sur le code source."""
return await (
dag.container()
.from_("python:3.12-slim")
.with_exec(["pip", "install", "ruff"])
.with_mounted_directory("/app", source)
.with_workdir("/app")
.with_exec(["ruff", "check", "src/", "tests/"])
.with_exec(["ruff", "format", "--check", "src/", "tests/"])
.stdout()
)
@function
async def test(self, source: dagger.Directory) -> str:
"""Exécute les tests unitaires avec pytest."""
return await (
dag.container()
.from_("python:3.12-slim")
.with_mounted_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["pip", "install", "pytest", "pytest-cov", "pytest-asyncio"])
.with_exec([
"pytest", "tests/",
"-v",
"--cov=src",
"--cov-report=term-missing",
"--cov-fail-under=80",
])
.stdout()
)
@function
async def test_with_db(
self,
source: dagger.Directory,
db_password: dagger.Secret,
) -> str:
"""Exécute les tests d'intégration avec une base PostgreSQL éphémère."""
# Démarrer un service PostgreSQL
postgres = (
dag.container()
.from_("postgres:16-alpine")
.with_env_variable("POSTGRES_DB", "testdb")
.with_env_variable("POSTGRES_USER", "testuser")
.with_secret_variable("POSTGRES_PASSWORD", db_password)
.with_exposed_port(5432)
.as_service()
)
# Exécuter les tests avec la DB comme service
return await (
dag.container()
.from_("python:3.12-slim")
.with_mounted_directory("/app", source)
.with_workdir("/app")
.with_service_binding("db", postgres)
.with_env_variable("DATABASE_URL", "postgresql://testuser:secret@db:5432/testdb")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["pip", "install", "pytest"])
.with_exec(["pytest", "tests/integration/", "-v"])
.stdout()
)
@function
async def build(self, source: dagger.Directory) -> dagger.Container:
"""Construit l'image Docker de l'application."""
return (
dag.container()
.from_("python:3.12-slim")
.with_exec(["apt-get", "update"])
.with_exec(["apt-get", "install", "-y", "--no-install-recommends", "curl"])
.with_exec(["apt-get", "clean"])
.with_mounted_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
.with_entrypoint(["python", "-m", "uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"])
.with_exposed_port(8000)
)
@function
async def publish(
self,
source: dagger.Directory,
registry: str,
image_name: str,
tag: str,
registry_user: str,
registry_password: dagger.Secret,
) -> str:
"""Construit et publie l'image sur un registre de conteneurs."""
container = await self.build(source)
ref = f"{registry}/{image_name}:{tag}"
digest = await (
container
.with_registry_auth(registry, registry_user, registry_password)
.publish(ref)
)
return f"Image publiée : {digest}"
@function
async def ci(self, source: dagger.Directory) -> str:
"""Pipeline CI complet : lint + test + build."""
# Lint
lint_result = await self.lint(source)
print(f"Lint OK:\n{lint_result}")
# Tests
test_result = await self.test(source)
print(f"Tests OK:\n{test_result}")
# Build
container = await self.build(source)
print("Build OK")
return "Pipeline CI terminé avec succès !"
@function
async def build_multiplatform(
self,
source: dagger.Directory,
registry: str,
image_name: str,
tag: str,
registry_user: str,
registry_password: dagger.Secret,
) -> str:
"""Construit et publie des images multi-architecture (amd64 + arm64)."""
platforms = ["linux/amd64", "linux/arm64"]
platform_variants = []
for platform in platforms:
ctr = (
dag.container(platform=dagger.Platform(platform))
.from_("python:3.12-slim")
.with_mounted_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
.with_entrypoint(["python", "-m", "uvicorn", "src.app:app", "--host", "0.0.0.0"])
.with_exposed_port(8000)
)
platform_variants.append(ctr)
ref = f"{registry}/{image_name}:{tag}"
digest = await dag.container().with_registry_auth(
registry, registry_user, registry_password
).publish(ref, platform_variants=platform_variants)
return f"Image multi-platform publiée : {digest}"Exécuter le Pipeline Localement
# Exécuter le lint
dagger call lint --source=.
# Exécuter les tests
dagger call test --source=.
# Exécuter le build
dagger call build --source=.
# Exécuter le pipeline CI complet
dagger call ci --source=.
# Exécuter les tests avec une base de données
dagger call test-with-db --source=. --db-password=env:DB_PASSWORD
# Publier l'image
dagger call publish \
--source=. \
--registry=ghcr.io \
--image-name=monorg/monapp \
--tag=v1.2.3 \
--registry-user=monuser \
--registry-password=env:GITHUB_TOKENLe point crucial ici : ces commandes fonctionnent exactement de la même manière sur votre laptop, dans GitHub Actions, dans GitLab CI, ou dans n'importe quel environnement avec Docker.
Le Daggerverse : Modules Réutilisables
Le Daggerverse est l'écosystème de modules Dagger réutilisables, comparable aux GitHub Actions Marketplace ou aux orbs CircleCI, mais avec une différence fondamentale : les modules Dagger sont des programmes compilés et typés, pas des scripts YAML.
Utiliser des Modules Existants
# Découvrir les modules disponibles
# https://daggerverse.dev
# Installer un module dans votre projet
dagger install github.com/purpleclay/daggerverse/golang@v0.5.0
dagger install github.com/sagikazarmark/daggerverse/helm@main
# Utiliser un module directement en ligne de commande
dagger -m github.com/shykes/daggerverse/wolfi@main \
call container --packages=python3,pip \
terminal
# Lister les modules installés
cat dagger.jsonCréer un Module Réutilisable
# Exemple d'un module de notification Slack
# dagger/src/main.py
import dagger
from dagger import dag, function, object_type
@object_type
class Notify:
"""Module de notification pour les pipelines CI/CD."""
@function
async def slack(
self,
webhook_url: dagger.Secret,
message: str,
channel: str = "#ci-cd",
status: str = "success",
) -> str:
"""Envoie une notification Slack via webhook."""
color = "#36a64f" if status == "success" else "#ff0000"
icon = ":white_check_mark:" if status == "success" else ":x:"
payload = f'{{"channel":"{channel}","attachments":[{{"color":"{color}","text":"{icon} {message}"}}]}}'
return await (
dag.container()
.from_("curlimages/curl:latest")
.with_secret_variable("WEBHOOK_URL", webhook_url)
.with_exec([
"sh", "-c",
f"curl -s -X POST \"$WEBHOOK_URL\" -H 'Content-Type: application/json' -d '{payload}'"
])
.stdout()
)
@function
async def email(
self,
smtp_host: str,
smtp_port: int,
smtp_user: str,
smtp_password: dagger.Secret,
to: str,
subject: str,
body: str,
) -> str:
"""Envoie une notification par email via SMTP."""
return await (
dag.container()
.from_("python:3.12-alpine")
.with_exec(["pip", "install", "secure-smtplib"])
.with_secret_variable("SMTP_PASSWORD", smtp_password)
.with_env_variable("SMTP_HOST", smtp_host)
.with_env_variable("SMTP_PORT", str(smtp_port))
.with_env_variable("SMTP_USER", smtp_user)
.with_exec([
"python", "-c",
f"""
import smtplib, os
from email.mime.text import MIMEText
msg = MIMEText('{body}')
msg['Subject'] = '{subject}'
msg['From'] = os.environ['SMTP_USER']
msg['To'] = '{to}'
with smtplib.SMTP(os.environ['SMTP_HOST'], int(os.environ['SMTP_PORT'])) as s:
s.starttls()
s.login(os.environ['SMTP_USER'], os.environ['SMTP_PASSWORD'])
s.send_message(msg)
print('Email envoyé')
"""
])
.stdout()
)Tester Localement Avant de Push
L'un des avantages majeurs de Dagger est de pouvoir exécuter et déboguer vos pipelines localement, exactement comme ils s'exécuteront en CI. Fini le cycle infernal « commit → push → attendre la CI → lire les logs → corriger → recommencer ».
# Exécuter une fonction spécifique
dagger call test --source=.
# Mode interactif : ouvrir un terminal dans le conteneur
dagger call build --source=. terminal
# Inspecter un conteneur intermédiaire
dagger call build --source=. directory --path=/app export --path=./output
# Visualiser le DAG d'exécution
dagger query <<EOF
{
container {
from(address: "python:3.12-slim") {
withExec(args: ["python", "--version"]) {
stdout
}
}
}
}
EOF
# Activer le debug verbose
dagger call --debug test --source=.Cette capacité de test local transforme radicalement le workflow de développement des pipelines CI/CD. Vous pouvez itérer en quelques secondes au lieu de plusieurs minutes, avec un feedback immédiat et la possibilité de debugger interactivement.
Intégration avec les Plateformes CI/CD
Dagger s'intègre avec n'importe quelle plateforme CI/CD existante. Le principe est simple : votre plateforme CI appelle les commandes Dagger, qui s'exécutent dans le Dagger Engine conteneurisé. Le fichier de configuration CI devient minimal — juste un wrapper autour de vos fonctions Dagger.
GitHub Actions
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
ci:
name: CI Pipeline
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Dagger CLI
uses: dagger/dagger-for-github@v7
with:
version: "latest"
- name: Run Lint
run: dagger call lint --source=.
- name: Run Tests
run: dagger call test --source=.
- name: Run Integration Tests
run: |
dagger call test-with-db \
--source=. \
--db-password=env:DB_TEST_PASSWORD
env:
DB_TEST_PASSWORD: testpassword
- name: Build and Publish (main only)
if: github.ref == 'refs/heads/main'
run: |
dagger call publish \
--source=. \
--registry=ghcr.io \
--image-name=${{ github.repository }} \
--tag=${{ github.sha }} \
--registry-user=${{ github.actor }} \
--registry-password=env:GITHUB_TOKEN
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}GitLab CI
# .gitlab-ci.yml
stages:
- ci
- deploy
variables:
DAGGER_VERSION: "latest"
.dagger-base:
image: docker:27-dind
services:
- docker:27-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
before_script:
- apk add --no-cache curl
- curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
- export PATH=$PATH:/root/.local/bin
lint:
extends: .dagger-base
stage: ci
script:
- dagger call lint --source=.
test:
extends: .dagger-base
stage: ci
script:
- dagger call test --source=.
build-and-publish:
extends: .dagger-base
stage: deploy
only:
- main
script:
- |
dagger call publish \
--source=. \
--registry=$CI_REGISTRY \
--image-name=$CI_PROJECT_PATH \
--tag=$CI_COMMIT_SHA \
--registry-user=$CI_REGISTRY_USER \
--registry-password=env:CI_REGISTRY_PASSWORD
variables:
CI_REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORDJenkins
# Jenkinsfile
pipeline {
agent {
docker {
image 'docker:27-dind'
args '--privileged -v /var/run/docker.sock:/var/run/docker.sock'
}
}
environment {
REGISTRY_PASSWORD = credentials('registry-password')
}
stages {
stage('Install Dagger') {
steps {
sh 'curl -fsSL https://dl.dagger.io/dagger/install.sh | sh'
sh 'export PATH=$PATH:$HOME/.local/bin'
}
}
stage('Lint') {
steps {
sh 'dagger call lint --source=.'
}
}
stage('Test') {
steps {
sh 'dagger call test --source=.'
}
}
stage('Build and Publish') {
when {
branch 'main'
}
steps {
sh '''
dagger call publish \
--source=. \
--registry=registry.exemple.fr \
--image-name=monapp \
--tag=${BUILD_NUMBER} \
--registry-user=jenkins \
--registry-password=env:REGISTRY_PASSWORD
'''
}
}
}
post {
always {
cleanWs()
}
}
}CircleCI
# .circleci/config.yml
version: 2.1
jobs:
ci:
machine:
image: ubuntu-2404:current
resource_class: medium
steps:
- checkout
- run:
name: Install Dagger
command: |
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
- run:
name: Lint
command: dagger call lint --source=.
- run:
name: Test
command: dagger call test --source=.
- run:
name: Build and Publish
command: |
dagger call publish \
--source=. \
--registry=ghcr.io \
--image-name=monorg/monapp \
--tag=${CIRCLE_SHA1} \
--registry-user=${REGISTRY_USER} \
--registry-password=env:REGISTRY_PASSWORD
workflows:
main:
jobs:
- ciRemarquez la beauté de l'approche : dans chaque plateforme CI, le fichier de configuration est minimal et quasi identique. La logique métier du pipeline est dans votre code Dagger, pas dans la configuration CI. Si vous migrez de GitHub Actions vers GitLab CI, seul le wrapper change — votre pipeline reste intact.
Caching Intelligent
Le caching est l'un des aspects les plus puissants de Dagger. Grâce à BuildKit, Dagger implémente un cache par contenu (content-addressable) qui sait exactement ce qui a changé et ne ré-exécute que le minimum nécessaire.
Cache Automatique
Par défaut, Dagger cache automatiquement :
- Les images de base :
from_("python:3.12-slim")n'est téléchargé qu'une fois - Les couches intermédiaires : Si vos dépendances n'ont pas changé,
pip installest caché - Les résultats d'exécution : Les commandes avec les mêmes entrées produisent un cache hit
Cache Explicite avec cache_volume
# Utilisation de volumes de cache pour les dépendances
@function
async def test_with_cache(self, source: dagger.Directory) -> str:
"""Tests avec cache des dépendances pip."""
pip_cache = dag.cache_volume("pip-cache")
return await (
dag.container()
.from_("python:3.12-slim")
.with_mounted_cache("/root/.cache/pip", pip_cache)
.with_mounted_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["pytest", "tests/", "-v"])
.stdout()
)
# Pour Node.js
@function
async def test_node(self, source: dagger.Directory) -> str:
"""Tests Node.js avec cache npm."""
npm_cache = dag.cache_volume("npm-cache")
node_modules = dag.cache_volume("node-modules")
return await (
dag.container()
.from_("node:20-slim")
.with_mounted_cache("/root/.npm", npm_cache)
.with_mounted_cache("/app/node_modules", node_modules)
.with_mounted_directory("/app", source)
.with_workdir("/app")
.with_exec(["npm", "ci"])
.with_exec(["npm", "test"])
.stdout()
)Optimisation de l'Ordre des Opérations
Comme avec les Dockerfiles, l'ordre des opérations impacte l'efficacité du cache :
# MAUVAIS : Le cache est invalidé à chaque changement de code
dag.container()
.from_("python:3.12-slim")
.with_mounted_directory("/app", source) # Change souvent
.with_exec(["pip", "install", "-r", ...]) # Ré-exécuté inutilement
.with_exec(["pytest", ...])
# BON : Les dépendances sont cachées indépendamment du code
dag.container()
.from_("python:3.12-slim")
.with_file("/app/requirements.txt",
source.file("requirements.txt")) # Change rarement
.with_workdir("/app")
.with_exec(["pip", "install", "-r", ...]) # Caché si requirements.txt n'a pas changé
.with_mounted_directory("/app", source) # Monté après l'install
.with_exec(["pytest", ...]) # Seuls les tests sont ré-exécutésGestion des Secrets
Dagger traite les secrets comme des citoyens de première classe. Les secrets ne sont jamais écrits dans les logs, le cache, ou les couches d'images.
# Passer un secret depuis une variable d'environnement
dagger call publish \
--registry-password=env:REGISTRY_TOKEN
# Passer un secret depuis un fichier
dagger call deploy \
--ssh-key=file:~/.ssh/id_ed25519
# Passer un secret depuis une commande
dagger call deploy \
--api-token=cmd:"vault kv get -field=token secret/api"
# Dans le code, les secrets sont typés
@function
async def deploy(
self,
source: dagger.Directory,
api_token: dagger.Secret, # Type dédié, jamais exposé
ssh_key: dagger.Secret,
) -> str:
return await (
dag.container()
.from_("alpine:latest")
.with_secret_variable("API_TOKEN", api_token) # Variable d'env secrète
.with_mounted_secret("/root/.ssh/id_ed25519", ssh_key) # Fichier secret
.with_exec(["sh", "-c", "echo 'Le token ne sera JAMAIS dans les logs'"])
.stdout()
)Services : Bases de Données de Test et Plus
Dagger permet de démarrer des services éphémères (bases de données, caches, APIs mock) comme partie intégrante de votre pipeline. Ces services sont automatiquement démarrés, connectés au réseau du pipeline, et nettoyés à la fin.
# Pipeline avec PostgreSQL + Redis comme services de test
@function
async def integration_tests(self, source: dagger.Directory) -> str:
"""Tests d'intégration avec PostgreSQL et Redis."""
# Service PostgreSQL
postgres = (
dag.container()
.from_("postgres:16-alpine")
.with_env_variable("POSTGRES_DB", "testdb")
.with_env_variable("POSTGRES_USER", "test")
.with_env_variable("POSTGRES_PASSWORD", "testpass")
.with_exposed_port(5432)
.as_service()
)
# Service Redis
redis = (
dag.container()
.from_("redis:7-alpine")
.with_exposed_port(6379)
.as_service()
)
# Service API mock (WireMock)
wiremock = (
dag.container()
.from_("wiremock/wiremock:latest")
.with_exposed_port(8080)
.with_mounted_directory(
"/home/wiremock/mappings",
source.directory("tests/wiremock-mappings"),
)
.as_service()
)
# Exécuter les tests avec tous les services
return await (
dag.container()
.from_("python:3.12-slim")
.with_service_binding("postgres", postgres)
.with_service_binding("redis", redis)
.with_service_binding("api-mock", wiremock)
.with_env_variable("DATABASE_URL", "postgresql://test:testpass@postgres:5432/testdb")
.with_env_variable("REDIS_URL", "redis://redis:6379/0")
.with_env_variable("EXTERNAL_API_URL", "http://api-mock:8080")
.with_mounted_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["pip", "install", "pytest"])
.with_exec(["pytest", "tests/integration/", "-v", "--tb=long"])
.stdout()
)Builds Multi-Platform
Construire des images pour plusieurs architectures (amd64, arm64) est natif avec Dagger, grâce à BuildKit et QEMU :
@function
async def build_all_platforms(
self,
source: dagger.Directory,
registry: str,
image: str,
tag: str,
username: str,
password: dagger.Secret,
) -> str:
"""Build et push multi-architecture."""
platforms = [
dagger.Platform("linux/amd64"),
dagger.Platform("linux/arm64"),
dagger.Platform("linux/arm/v7"),
]
variants = []
for platform in platforms:
ctr = (
dag.container(platform=platform)
.from_("python:3.12-slim")
.with_file("/app/requirements.txt", source.file("requirements.txt"))
.with_workdir("/app")
.with_exec(["pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
.with_mounted_directory("/app", source)
.with_entrypoint(["python", "-m", "uvicorn", "src.app:app"])
.with_exposed_port(8000)
)
variants.append(ctr)
ref = f"{registry}/{image}:{tag}"
digest = await (
dag.container()
.with_registry_auth(registry, username, password)
.publish(ref, platform_variants=variants)
)
return f"Manifest multi-arch publié: {digest}"Comparaison avec les Outils CI/CD Traditionnels
| Critère | Dagger | GitHub Actions | GitLab CI | Tekton |
|---|---|---|---|---|
| Langage de définition | Go, Python, TS, PHP | YAML | YAML | YAML (CRDs K8s) |
| Portabilité | Totale | GitHub uniquement | GitLab uniquement | Kubernetes |
| Exécution locale | Oui (natif) | Partiel (act) | Partiel (gitlab-runner) | Oui (si K8s local) |
| Typage et autocomplétion | Oui | Non (YAML) | Non (YAML) | Non (YAML) |
| Tests unitaires du pipeline | Oui (tests classiques) | Non | Non | Non |
| Caching | Automatique (BuildKit) | Manuel (actions/cache) | Manuel (cache:) | Manuel |
| Secrets | Type dédié, jamais exposés | Secrets GitHub | Variables CI/CD | Secrets K8s |
| Services éphémères | Natif | services: | services: | Sidecars |
| Debugging | Breakpoints, terminal interactif | SSH debug (payant) | SSH debug | kubectl logs |
| Courbe d'apprentissage | Moyenne (SDK) | Faible (YAML) | Faible (YAML) | Élevée (K8s) |
| Communauté | En croissance rapide | Très large | Large | Moyenne |
Quand Choisir Dagger ?
- Votre pipeline est complexe : Si votre CI/CD dépasse les cas simples « build → test → deploy », la programmabilité de Dagger prend tout son sens
- Vous voulez tester localement : Le « commit → push → attendre → vérifier » vous rend fou
- Multi-plateforme CI : Vous avez des projets sur GitHub, GitLab, et Jenkins et voulez unifier vos pipelines
- Équipe DevOps/Platform : Vous créez des pipelines réutilisables pour plusieurs équipes
- Migration à venir : Vous savez que vous changerez de plateforme CI et ne voulez pas réécrire vos pipelines
Quand les Outils Traditionnels Suffisent
- Pipelines simples : Un build → test → deploy basique est plus rapide à écrire en YAML
- Petite équipe mono-plateforme : Si vous êtes 100% GitHub et satisfait, l'overhead de Dagger n'est pas justifié
- Pas de Docker disponible : Dagger nécessite un runtime de conteneurs
Cas d'Usage Concrets
1. Monorepo avec Builds Sélectifs
@function
async def ci_monorepo(self, source: dagger.Directory) -> str:
"""Pipeline CI pour un monorepo : ne build que ce qui a changé."""
results = []
# Vérifier si le service API a changé
api_dir = source.directory("services/api")
try:
api_result = await (
dag.container()
.from_("golang:1.23-alpine")
.with_mounted_directory("/app", api_dir)
.with_workdir("/app")
.with_exec(["go", "test", "./..."])
.stdout()
)
results.append(f"API: OK\n{api_result}")
except Exception as e:
results.append(f"API: FAILED\n{e}")
# Vérifier si le frontend a changé
web_dir = source.directory("services/web")
try:
web_result = await (
dag.container()
.from_("node:20-slim")
.with_mounted_directory("/app", web_dir)
.with_workdir("/app")
.with_exec(["npm", "ci"])
.with_exec(["npm", "run", "lint"])
.with_exec(["npm", "test"])
.stdout()
)
results.append(f"Web: OK\n{web_result}")
except Exception as e:
results.append(f"Web: FAILED\n{e}")
return "\n---\n".join(results)2. Pipeline de Déploiement Kubernetes
@function
async def deploy_k8s(
self,
source: dagger.Directory,
kubeconfig: dagger.Secret,
image_tag: str,
namespace: str = "production",
) -> str:
"""Déploie sur Kubernetes avec kubectl."""
return await (
dag.container()
.from_("bitnami/kubectl:latest")
.with_mounted_secret("/root/.kube/config", kubeconfig)
.with_mounted_directory("/manifests", source.directory("k8s/"))
.with_workdir("/manifests")
.with_exec([
"sh", "-c",
f"sed -i 's|IMAGE_TAG|{image_tag}|g' deployment.yaml && "
f"kubectl apply -f . -n {namespace} && "
f"kubectl rollout status deployment/app -n {namespace} --timeout=300s"
])
.stdout()
)3. Security Scanning
@function
async def security_scan(self, source: dagger.Directory) -> str:
"""Scanne les vulnérabilités avec Trivy et les secrets avec Gitleaks."""
# Scan de vulnérabilités avec Trivy
trivy_result = await (
dag.container()
.from_("aquasec/trivy:latest")
.with_mounted_directory("/app", source)
.with_exec([
"trivy", "fs",
"--severity", "HIGH,CRITICAL",
"--exit-code", "1",
"--format", "table",
"/app"
])
.stdout()
)
# Détection de secrets avec Gitleaks
gitleaks_result = await (
dag.container()
.from_("zricethezav/gitleaks:latest")
.with_mounted_directory("/app", source)
.with_exec([
"gitleaks", "detect",
"--source=/app",
"--no-git",
"--report-format=json",
"--exit-code=1",
])
.stdout()
)
return f"Trivy:\n{trivy_result}\n\nGitleaks:\n{gitleaks_result}"Performances
Dagger offre d'excellentes performances grâce à plusieurs mécanismes :
Parallélisation Automatique
Le DAG d'exécution de Dagger identifie automatiquement les opérations indépendantes et les exécute en parallèle. Si votre lint et vos tests n'ont pas de dépendances, ils tourneront simultanément.
Cache Incrémental
Le cache BuildKit est extrêmement efficace. Lors des exécutions suivantes, seules les étapes dont les entrées ont changé sont ré-exécutées. Pour un pipeline typique, un second run est 5 à 10 fois plus rapide que le premier.
Benchmarks Typiques
| Opération | Premier run | Runs suivants (cache) |
|---|---|---|
| Build Python + dépendances | 45-90s | 5-15s |
| Suite de tests (pytest) | 30-60s | 30-60s (toujours exécuté) |
| Build image Docker | 60-120s | 5-10s (si code seul change) |
| Push vers registre | 30-60s | 5-10s (couches cachées) |
| Pipeline complet | 3-5 min | 1-2 min |
Limites et Considérations
Dagger n'est pas exempt de limitations qu'il est important de connaître :
Limites Techniques
- Dépendance à Docker : Un runtime de conteneurs est obligatoire. Pas de Docker = pas de Dagger.
- Overhead du moteur : Le Dagger Engine consomme des ressources. Pour des pipelines très simples, c'est de l'overhead inutile.
- Courbe d'apprentissage : Maîtriser le SDK demande plus d'effort que d'écrire du YAML basique. L'investissement se rentabilise sur les pipelines complexes.
- Maturité des SDKs : Le SDK Go est le plus mature. Python et TypeScript sont stables mais certains cas limites existent. PHP reste expérimental.
- Debugging complexe : Quand un problème survient dans le Dagger Engine lui-même (et non dans votre code), le debugging peut être ardu.
Limites Organisationnelles
- Adoption en équipe : Si votre équipe maîtrise déjà bien GitHub Actions, la migration vers Dagger représente un changement de paradigme qui nécessite formation et adhésion.
- Écosystème plus jeune : Le Daggerverse est beaucoup moins fourni que le GitHub Actions Marketplace. Vous devrez parfois écrire vos propres modules.
- Documentation : Bien qu'en amélioration constante, la documentation est parfois en retard par rapport aux fonctionnalités.
Bonnes Pratiques
1. Structurer Vos Modules
# Structure recommandée pour un projet Dagger
mon-projet/
├── dagger/
│ └── src/
│ └── main.py # Pipeline principal
├── dagger.json # Configuration du module
├── src/ # Code applicatif
├── tests/ # Tests applicatifs
└── .github/workflows/
└── ci.yml # Wrapper CI minimal2. Séparer les Fonctions
# Préférez des fonctions atomiques et composables
# plutôt qu'une seule fonction monolithique
@function
async def lint(self, source: dagger.Directory) -> str: ...
@function
async def test(self, source: dagger.Directory) -> str: ...
@function
async def build(self, source: dagger.Directory) -> dagger.Container: ...
@function
async def publish(self, container: dagger.Container, ...) -> str: ...
@function
async def ci(self, source: dagger.Directory) -> str:
"""Compose les fonctions atomiques."""
await self.lint(source)
await self.test(source)
ctr = await self.build(source)
return "CI OK"3. Toujours Tester Localement
# Prenez l'habitude de tester chaque fonction localement
dagger call lint --source=.
dagger call test --source=.
dagger call build --source=.
# Avant de commit, lancez le pipeline complet
dagger call ci --source=.4. Versionner le SDK
# Fixez la version de Dagger dans votre CI
# .github/workflows/ci.yml
- uses: dagger/dagger-for-github@v7
with:
version: "0.15.1" # Version fixe, pas "latest"Conclusion
Dagger représente un changement de paradigme dans la façon de concevoir les pipelines CI/CD. En remplaçant les fichiers YAML propriétaires par du code dans un vrai langage de programmation, exécuté dans des conteneurs via un moteur standardisé, Dagger apporte les bénéfices que Docker a apportés au déploiement : portabilité, reproductibilité et standardisation.
Le projet n'est pas sans défauts — la courbe d'apprentissage est réelle, la dépendance à Docker est contraignante dans certains environnements, et l'écosystème est encore jeune comparé aux mastodontes du marché. Mais pour les équipes qui gèrent des pipelines complexes, qui travaillent sur plusieurs plateformes CI, ou qui sont fatiguées du cycle « push → attendre → debug », Dagger est une proposition de valeur difficile à ignorer.
En 2026, avec des SDKs stables en Go, Python et TypeScript, un Daggerverse en pleine expansion, et le soutien de Solomon Hykes et de son équipe, Dagger est positionné pour devenir un standard de l'industrie. La recommandation est simple :
- Évaluez Dagger sur un projet existant — commencez par conteneuriser votre pipeline actuel
- Testez localement — c'est la fonctionnalité qui convertit la plupart des développeurs
- Adoptez progressivement — Dagger n'est pas du tout-ou-rien, il coexiste avec vos outils existants
- Contribuez au Daggerverse — Partagez vos modules pour enrichir l'écosystème
La CI/CD programmable et portable n'est plus un concept théorique — c'est une réalité que vous pouvez adopter dès aujourd'hui.
Ressources utiles :
Site officiel Dagger
Documentation Dagger
GitHub Dagger
Daggerverse (modules)
Discord Dagger