TL;DR — Résumé Rapide

Dagger vous permet d'écrire des pipelines CI/CD en Go, Python ou TypeScript qui s'exécutent identiquement sur votre laptop et sur toute plateforme CI.

Dagger est un moteur CI/CD programmable qui exécute la logique de votre pipeline dans des conteneurs, en utilisant de vrais langages de programmation plutôt que du YAML. Si vous avez déjà passé des heures à déboguer un échec sur GitHub Actions qui ne se reproduisait jamais en local, ou maintenu cinq configurations de pipeline légèrement différentes sur GitHub, GitLab et Jenkins, Dagger résout les deux problèmes à la fois : votre pipeline complet est du code typé qui s’exécute de manière identique sur votre laptop et sur n’importe quelle plateforme CI. Ce guide couvre l’architecture, les patterns du SDK, les services, le cache, les secrets et l’intégration CI.

Prérequis

  • Docker Desktop ou Docker Engine en cours d’exécution localement
  • Node.js 18+ (pour le SDK TypeScript), Go 1.21+ ou Python 3.11+ selon le SDK choisi
  • Familiarité de base avec les conteneurs et au moins l’un des langages Go, Python ou TypeScript
  • Un projet à construire et tester (une application Node.js est utilisée dans les exemples)

Pourquoi Dagger : Le Problème de Lock-In Fournisseur

Chaque plateforme CI majeure possède son propre DSL pour les pipelines. GitHub Actions utilise du YAML avec des steps uses:, GitLab CI utilise des stages dans .gitlab-ci.yml, Jenkins utilise un DSL Déclaratif basé sur Groovy, et CircleCI utilise encore un autre schéma YAML. Le résultat est que vos connaissances et votre code de pipeline sont totalement non portables.

Le second problème est la brèche «ça marche sur ma machine». Un système CI est un environnement différent de votre machine de développement. Déboguer un pipeline en échec signifie pousser un commit, attendre un runner, lire des logs tronqués, puis recommencer. Vous ne pouvez pas exécuter GitHub Actions localement de manière significative sans contournements complexes.

Dagger résout cela avec trois décisions de conception :

  1. Les pipelines sont natifs des conteneurs. Chaque étape s’exécute dans un conteneur via BuildKit. Le cache, l’isolation et la portabilité viennent du modèle de conteneurs, pas des abstractions des plateformes CI.
  2. Les pipelines sont du vrai code. Vous écrivez Go, Python ou TypeScript. Vous obtenez des types, des tests, le support d’IDE et la réutilisation du code — des choses que YAML ne peut pas offrir.
  3. L’intégration CI est un wrapper fin. Votre YAML CI devient une seule invocation dagger call. Toute la logique reste dans votre code.
FonctionnalitéDaggerGitHub ActionsGitLab CIJenkinsEarthly
Exécution locale sans modificationOuiNonNonPartielOui
Langage pour la logique de pipelineGo/Python/TSYAMLYAMLGroovyDSL Earthfile
Lock-in fournisseurAucunÉlevéÉlevéMoyenFaible
Cache natif de conteneursCouches BuildKitLimitéLimitéAucunCouches BuildKit
Écosystème de modules réutilisablesDaggerverseActions MarketplaceAucunPluginsAucun
Sécurité des typesComplèteNonNonPartielleNon
Support IDECompletExtensionsExtensionsExtensionsAucun

Architecture : Comment Fonctionne Dagger

L’architecture de Dagger comporte trois couches.

Le Moteur Dagger est un daemon de longue durée qui encapsule BuildKit. Lorsque vous exécutez dagger call, le CLI se connecte au Moteur, qui exécute les étapes de votre pipeline dans des conteneurs isolés. Le Moteur expose une API GraphQL sur un socket Unix. Tous les clients SDK communiquent avec lui via cette API.

Les clients SDK (Go, Python, TypeScript, Elixir) génèrent des wrappers fortement typés autour de l’API GraphQL. Lorsque votre code TypeScript appelle dag.container().withExec(["npm", "test"]), il construit une requête GraphQL envoyée au Moteur. Le Moteur l’évalue de manière lazy — rien ne s’exécute tant que vous n’appelez pas .stdout() ou .sync() pour récupérer un résultat, ce qui permet à Dagger d’optimiser le graphe d’exécution.

Les Modules Dagger sont l’unité de packaging. Un module est un répertoire contenant un manifeste dagger.json, le code de votre pipeline et ses dépendances. Les modules peuvent être publiés sur le Daggerverse (dagger.io/hub) et consommés par d’autres modules avec dagger install.

Installation

Installez le CLI dagger avec le script officiel :

curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh
dagger version

Sur macOS avec Homebrew :

brew install dagger/tap/dagger

Docker doit être en cours d’exécution. Dagger utilise Docker (ou tout daemon Moby compatible) comme backend de conteneurs. Vérifiez la connectivité :

docker info && dagger version

Écriture de Pipelines en TypeScript

Initialisez un nouveau module Dagger dans votre projet :

dagger init --sdk=typescript --name=mon-pipeline

Cela crée dagger.json et src/index.ts. Un pipeline complet de build et de tests pour une application Node.js :

import { dag, Container, Directory, object, func } from "@dagger.io/dagger";

@object()
export class MonPipeline {
  @func()
  async build(source: Directory): Promise<Container> {
    const nodeCache = dag.cacheVolume("node-modules");

    return dag
      .container()
      .from("node:20-alpine")
      .withMountedDirectory("/app", source)
      .withMountedCache("/app/node_modules", nodeCache)
      .withWorkdir("/app")
      .withExec(["npm", "ci"])
      .withExec(["npm", "run", "build"]);
  }

  @func()
  async test(source: Directory): Promise<string> {
    const built = await this.build(source);
    return built
      .withExec(["npm", "test", "--", "--forceExit"])
      .stdout();
  }
}

Exécutez localement en passant votre répertoire courant comme source :

dagger call test --source=.

L’appel withMountedCache crée un volume de cache persistant pour node_modules qui survit entre les exécutions du pipeline.

Services : Bases de Données et Redis dans les Tests

L’abstraction Service de Dagger vous permet d’attacher des conteneurs éphémères comme services accessibles sur le réseau pendant une exécution du pipeline. Cela élimine le besoin de bases de données simulées dans les tests d’intégration.

@func()
async integrationTest(source: Directory): Promise<string> {
  const postgres = dag
    .container()
    .from("postgres:16-alpine")
    .withEnvVariable("POSTGRES_PASSWORD", "test")
    .withEnvVariable("POSTGRES_DB", "testdb")
    .withExposedPort(5432)
    .asService();

  return dag
    .container()
    .from("node:20-alpine")
    .withMountedDirectory("/app", source)
    .withWorkdir("/app")
    .withServiceBinding("db", postgres)
    .withEnvVariable("DATABASE_URL", "postgresql://postgres:test@db:5432/testdb")
    .withExec(["npm", "ci"])
    .withExec(["npm", "run", "test:integration"])
    .stdout();
}

Gestion des Secrets

Dagger possède un type Secret de première classe qui garantit que les valeurs sensibles ne sont jamais écrites dans les logs, mises en cache ou exposées dans les traces.

Passez un secret depuis le CLI :

dagger call publish --source=. --registry-token=env:REGISTRY_TOKEN

Utilisez-le dans votre pipeline TypeScript :

@func()
async publish(source: Directory, registryToken: Secret): Promise<string> {
  return dag
    .container()
    .from("node:20-alpine")
    .withMountedDirectory("/app", source)
    .withWorkdir("/app")
    .withExec(["npm", "ci"])
    .withExec(["npm", "run", "build"])
    .withSecretVariable("NPM_TOKEN", registryToken)
    .withExec(["npm", "publish"])
    .stdout();
}

Intégration CI

La stratégie d’intégration CI de Dagger est toujours la même : maintenir le YAML CI aussi fin que possible. Toute la logique reste dans le module.

GitHub Actions :

name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dagger/dagger-for-github@v6
        with:
          verb: call
          args: test --source=.
          cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }}

GitLab CI :

test:
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh
  script:
    - dagger call test --source=.

Dans tous les cas, la commande dagger call test --source=. est identique. Changer de fournisseur CI nécessite uniquement de modifier le wrapper fin — pas de réécrire la logique du pipeline.

Scénario Réel : Pipeline Node.js Complet

Vous avez une API Node.js nécessitant du linting, des tests unitaires avec une base de données PostgreSQL, un build d’image Docker et un push vers GitHub Container Registry à chaque merge sur main.

@func()
async ci(
  source: Directory,
  registryToken: Secret,
  branch: string
): Promise<string> {
  const [lintResult, testResult] = await Promise.all([
    this.lint(source),
    this.integrationTest(source),
  ]);

  if (branch !== "main") {
    return `lint: ok\ntests: ok\npublication ignorée (branche: ${branch})`;
  }

  const ref = await dag
    .container()
    .from("node:20-alpine")
    .withMountedDirectory("/app", source)
    .withWorkdir("/app")
    .withExec(["npm", "ci"])
    .withExec(["npm", "run", "build"])
    .withRegistryAuth("ghcr.io", "github-actions", registryToken)
    .publish("ghcr.io/mon-org/mon-api:latest");

  return `publié : ${ref}`;
}

Pièges et Cas Limites

Docker-in-Docker sur CI — Dagger nécessite l’accès à un daemon Docker. Sur GitHub Actions avec des runners ubuntu-latest, Docker est préinstallé. Sur GitLab CI, utilisez le service docker:dind et configurez DOCKER_HOST=tcp://docker:2376.

Grands monorepos — Passer un répertoire monorepo complet en tant qu’input Directory copie tous les fichiers dans le système de fichiers Dagger. Utilisez withDirectory avec un filtre ou passez uniquement le sous-répertoire dont votre pipeline a besoin pour maintenir des transferts rapides.

Invalidation de cache — Les caches withMountedCache sont identifiés par le nom du volume. Si vous voulez des caches séparés par branche, utilisez un nom de volume dynamique comme node-modules-${branch}.

Support Windows — Dagger nécessite un backend de conteneurs Linux. Sur Windows, Docker Desktop avec WSL2 est requis. Le CLI dagger s’exécute nativement sur Windows, mais l’exécution du pipeline se fait toujours dans des conteneurs Linux.

Résumé

  • Dagger résout le lock-in fournisseur CI en déplaçant la logique du pipeline vers du code natif de conteneurs (Go, Python, TypeScript)
  • Le Moteur Dagger encapsule BuildKit et expose une API GraphQL ; les clients SDK fournissent des wrappers typés dans votre langage de prédilection
  • withMountedCache offre un cache persistant entre exécutions pour les dépendances sans plugins de cache CI personnalisés
  • Les services (asService + withServiceBinding) fournissent de vraies bases de données et Redis pour les tests d’intégration sans mocks
  • Les secrets utilisent un paramètre typé Secret qui n’est jamais journalisé ni mis en cache
  • L’intégration CI est toujours un wrapper fin appelant dagger call — commande identique sur GitHub Actions, GitLab CI, CircleCI et Jenkins
  • Les Modules Dagger sont publiables sur le Daggerverse et composables — réutilisez des composants de pipeline de la communauté
  • Dagger Cloud ajoute la visualisation de traces, le partage de cache distribué persistant et la surveillance TUI en temps réel

Articles Connexes