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:
- 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.
- 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.
- CI-Integration ist ein dünner Wrapper. Das CI-YAML wird zu einem einzelnen
dagger call-Aufruf. Alle Logik bleibt im Code.
| Merkmal | Dagger | GitHub Actions | GitLab CI | Jenkins | Earthly |
|---|---|---|---|---|---|
| Lokal ohne Änderung ausführbar | Ja | Nein | Nein | Teilweise | Ja |
| Sprache für Pipeline-Logik | Go/Python/TS | YAML | YAML | Groovy | Earthfile-DSL |
| Vendor-Lock-in | Keiner | Hoch | Hoch | Mittel | Gering |
| Container-natives Caching | BuildKit-Ebenen | Begrenzt | Begrenzt | Keines | BuildKit-Ebenen |
| Wiederverwendbare Module | Daggerverse | Actions Marketplace | Keines | Plugins | Keines |
| Typsicherheit | Vollständig | Nein | Nein | Teilweise | Nein |
| IDE-Unterstützung | Vollständig | Erweiterungen | Erweiterungen | Erweiterungen | Keine |
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-Invalidierung — withMountedCache-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
withMountedCachebietet 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 callaufruft — 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