TL;DR — Kurzzusammenfassung

Dagger ermöglicht CI/CD-Pipelines als Code in Go, Python oder TypeScript zu schreiben, die auf dem Laptop und in jeder CI-Plattform identisch laufen.

Dagger ist eine programmierbare CI/CD-Engine, die Pipeline-Logik in Containern ausführt — mit echten Programmiersprachen statt YAML. Wer schon Stunden damit verbracht hat, einen GitHub Actions-Fehler zu debuggen, der sich lokal nie reproduzieren ließ, oder fünf leicht unterschiedliche Pipeline-Konfigurationen für GitHub, GitLab und Jenkins gepflegt hat, dem löst Dagger beide Probleme auf einmal: Die gesamte Pipeline ist typisierter Code, der auf dem Laptop und in jeder CI-Plattform identisch ausgeführt wird. Dieser Leitfaden behandelt Architektur, SDK-Muster, Services, Caching, Secrets und CI-Integration.

Voraussetzungen

  • Docker Desktop oder Docker Engine läuft lokal
  • Node.js 18+ (für das TypeScript-SDK), Go 1.21+ oder Python 3.11+ je nach gewähltem SDK
  • Grundkenntnisse mit Containern und mindestens einer der Sprachen Go, Python oder TypeScript
  • Ein Projekt zum Bauen und Testen (eine Node.js-App wird in den Beispielen verwendet)

Warum Dagger: Das Vendor-Lock-in-Problem

Jede große CI-Plattform hat ihr eigenes Pipeline-DSL. GitHub Actions verwendet YAML mit uses:-Steps, GitLab CI verwendet Stages in .gitlab-ci.yml, Jenkins verwendet ein Groovy-basiertes Declarative Pipeline DSL, und CircleCI verwendet noch ein weiteres YAML-Schema. Das Ergebnis: Pipeline-Wissen und Pipeline-Code sind vollständig nicht portabel.

Das zweite Problem ist die «funktioniert auf meinem Rechner»-Lücke. Ein CI-System ist eine andere Umgebung als die Entwicklungsmaschine. Das Debuggen einer fehlgeschlagenen Pipeline bedeutet: Commit pushen, auf einen Runner warten, abgeschnittene Logs lesen, wiederholen. GitHub Actions lokal auszuführen ist ohne komplexe Workarounds kaum möglich.

Dagger löst das mit drei Designentscheidungen:

  1. Pipelines sind Container-nativ. Jeder Schritt läuft in einem Container via BuildKit. Caching, Isolation und Portabilität kommen aus dem Container-Modell, nicht aus CI-Plattform-Abstraktionen.
  2. Pipelines sind echter Code. Sie schreiben Go, Python oder TypeScript. Sie erhalten Typen, Tests, IDE-Unterstützung und Wiederverwendbarkeit — Dinge, die YAML nicht bieten kann.
  3. CI-Integration ist ein dünner Wrapper. Das CI-YAML wird zu einem einzelnen dagger call-Aufruf. Alle Logik bleibt im Code.
MerkmalDaggerGitHub ActionsGitLab CIJenkinsEarthly
Lokal ohne Änderung ausführbarJaNeinNeinTeilweiseJa
Sprache für Pipeline-LogikGo/Python/TSYAMLYAMLGroovyEarthfile-DSL
Vendor-Lock-inKeinerHochHochMittelGering
Container-natives CachingBuildKit-EbenenBegrenztBegrenztKeinesBuildKit-Ebenen
Wiederverwendbare ModuleDaggerverseActions MarketplaceKeinesPluginsKeines
TypsicherheitVollständigNeinNeinTeilweiseNein
IDE-UnterstützungVollständigErweiterungenErweiterungenErweiterungenKeine

Architektur: Wie Dagger Funktioniert

Die Dagger-Architektur besteht aus drei Schichten.

Die Dagger Engine ist ein langlebiger Daemon, der BuildKit kapselt. Wenn dagger call ausgeführt wird, verbindet sich das CLI mit der Engine, die Pipeline-Schritte in isolierten Containern ausführt. Die Engine exponiert eine GraphQL-API über einen Unix-Socket. Alle SDK-Clients kommunizieren über diese API.

Die SDK-Clients (Go, Python, TypeScript, Elixir) generieren stark typisierte Wrapper um die GraphQL-API. Wenn TypeScript-Code dag.container().withExec(["npm", "test"]) aufruft, erstellt er eine GraphQL-Anfrage, die an die Engine gesendet wird. Die Engine wertet diese lazy aus — nichts wird ausgeführt, bis .stdout() oder .sync() zum Abrufen eines Ergebnisses aufgerufen wird, was Dagger ermöglicht, den Ausführungsgraphen zu optimieren.

Dagger-Module sind die Packaging-Einheit. Ein Modul ist ein Verzeichnis mit einem dagger.json-Manifest, dem Pipeline-Code und Abhängigkeiten. Module können im Daggerverse (dagger.io/hub) veröffentlicht und von anderen Modulen mit dagger install konsumiert werden.

Installation

Installieren Sie das dagger CLI mit dem offiziellen Skript:

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

Auf macOS mit Homebrew:

brew install dagger/tap/dagger

Docker muss laufen. Dagger verwendet Docker (oder jeden kompatiblen Moby-Daemon) als Container-Backend. Konnektivität prüfen:

docker info && dagger version

Pipelines in TypeScript Schreiben

Initialisieren Sie ein neues Dagger-Modul im Projekt:

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

Dies erstellt dagger.json und src/index.ts. Eine vollständige Build-und-Test-Pipeline für eine Node.js-App:

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

@object()
export class MeinePipeline {
  @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();
  }
}

Lokal ausführen, indem das aktuelle Verzeichnis als Quelle übergeben wird:

dagger call test --source=.

Der withMountedCache-Aufruf erstellt ein persistentes Cache-Volume für node_modules, das zwischen Pipeline-Ausführungen erhalten bleibt.

Services: Datenbanken und Redis in Tests

Daggers Service-Abstraktion ermöglicht das Anhängen ephemerer Container als netzwerkzugängliche Services während einer Pipeline-Ausführung. Dadurch entfällt die Notwendigkeit gemockter Datenbanken in Integrationstests.

@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();
}

Secrets-Verwaltung

Dagger hat einen erstklassigen Secret-Typ, der sicherstellt, dass sensible Werte niemals in Logs geschrieben, gecacht oder in Traces exponiert werden.

Ein Secret vom CLI übergeben:

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

Im TypeScript-Pipeline verwenden:

@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();
}

CI-Integration

Daggers CI-Integrationsstrategie ist immer dieselbe: das CI-YAML so dünn wie möglich halten. Alle Logik bleibt im Modul.

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=.

In allen Fällen ist der Befehl dagger call test --source=. identisch. Ein CI-Anbieterwechsel erfordert nur die Änderung des dünnen Wrappers — nicht das Umschreiben der Pipeline-Logik.

Praxisbeispiel: Vollständige Node.js-Pipeline

Sie haben eine Node.js-API, die Linting, Unit-Tests mit einer PostgreSQL-Datenbank, einen Docker-Image-Build und einen Push zu GitHub Container Registry bei jedem Merge auf main benötigt.

@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\nveröffentlichung übersprungen (branch: ${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/meine-org/meine-api:latest");

  return `veröffentlicht: ${ref}`;
}

Fallstricke und Sonderfälle

Docker-in-Docker auf CI — Dagger benötigt Zugang zu einem Docker-Daemon. Auf GitHub Actions mit ubuntu-latest-Runnern ist Docker vorinstalliert. Auf GitLab CI den docker:dind-Service verwenden und DOCKER_HOST=tcp://docker:2376 konfigurieren.

Große Monorepos — Ein gesamtes Monorepo-Verzeichnis als Directory-Input zu übergeben, kopiert alle Dateien in das Dagger-Dateisystem. withDirectory mit einem Filter verwenden oder nur das benötigte Unterverzeichnis übergeben, um Transfers schnell zu halten.

Cache-InvalidierungwithMountedCache-Caches werden durch den Volume-Namen identifiziert. Für separate Caches pro Branch einen dynamischen Volume-Namen wie node-modules-${branch} verwenden.

Windows-Unterstützung — Dagger benötigt ein Linux-Container-Backend. Auf Windows ist Docker Desktop mit WSL2 erforderlich. Das dagger CLI läuft nativ auf Windows, aber die Pipeline-Ausführung findet immer in Linux-Containern statt.

Zusammenfassung

  • Dagger löst CI-Vendor-Lock-in, indem Pipeline-Logik in Container-nativen Code (Go, Python, TypeScript) verlagert wird
  • Die Dagger Engine kapselt BuildKit und exponiert eine GraphQL-API; SDK-Clients bieten typisierte Wrapper in der bevorzugten Sprache
  • withMountedCache bietet persistentes Caching zwischen Ausführungen für Abhängigkeiten ohne benutzerdefinierte CI-Cache-Plugins
  • Services (asService + withServiceBinding) stellen echte Datenbanken und Redis für Integrationstests ohne Mocks bereit
  • Secrets verwenden einen typisierten Secret-Parameter, der niemals geloggt oder gecacht wird
  • CI-Integration ist immer ein dünner Wrapper, der dagger call aufruft — identischer Befehl für GitHub Actions, GitLab CI, CircleCI und Jenkins
  • Dagger-Module sind im Daggerverse veröffentlichbar und kombinierbar — Community-Pipeline-Komponenten wiederverwenden
  • Dagger Cloud fügt Trace-Visualisierung, persistentes verteiltes Cache-Sharing und TUI-Monitoring in Echtzeit hinzu

Verwandte Artikel