Dagger vs Earthly vs Depot: Programmable CI/CD Engines Compared (2026)
TL;DR: Dagger is the programmable CI/CD engine — write pipelines in TypeScript, Python, or Go, run them anywhere (locally and in CI), and cache everything in containers. Earthly combines Dockerfiles and Makefiles into Earthfiles — reproducible, containerized builds with native caching that work identically on your laptop and in CI. Depot is the remote build service — faster Docker builds with persistent caching, native multi-platform support, and drop-in replacement for docker build. In 2026: Dagger for code-first CI/CD pipelines, Earthly for reproducible containerized builds, Depot for faster Docker image builds.
Key Takeaways
- Dagger: SDK-based (TypeScript, Python, Go). Write pipelines as code, run locally or in any CI. Container-native execution, automatic caching, module ecosystem. Best for teams wanting portable CI/CD pipelines written in real programming languages
- Earthly: Earthfile syntax (Dockerfile + Makefile). Reproducible builds, shared caching, parallel execution. Best for monorepo builds and teams wanting Makefile-like workflows in containers
- Depot: Remote Docker build service. Persistent build cache, native ARM/x86 builders, BuildKit optimized. Best for teams needing faster Docker builds without infrastructure changes
Dagger — Programmable CI/CD Engine
Dagger lets you write CI/CD pipelines in TypeScript, Python, or Go — run them locally for fast iteration, then deploy to any CI platform.
TypeScript Pipeline
// ci/index.ts — Dagger pipeline in TypeScript
import { dag, Container, Directory, object, func } from "@dagger.io/dagger";
@object()
class CI {
// Lint the codebase
@func()
async lint(source: Directory): Promise<string> {
return dag
.container()
.from("node:20-slim")
.withDirectory("/app", source)
.withWorkdir("/app")
.withExec(["npm", "ci"])
.withExec(["npm", "run", "lint"])
.stdout();
}
// Run tests
@func()
async test(source: Directory): Promise<string> {
return dag
.container()
.from("node:20-slim")
.withDirectory("/app", source)
.withWorkdir("/app")
.withExec(["npm", "ci"])
.withExec(["npm", "run", "test"])
.stdout();
}
// Build the application
@func()
async build(source: Directory): Promise<Container> {
const deps = dag
.container()
.from("node:20-slim")
.withDirectory("/app", source)
.withWorkdir("/app")
.withExec(["npm", "ci"]);
return deps
.withExec(["npm", "run", "build"])
.withEntrypoint(["node", "dist/server.js"]);
}
// Full CI pipeline — lint, test, build, push
@func()
async ci(source: Directory, registryToken: string): Promise<string> {
// Run lint and test in parallel
const [lintResult, testResult] = await Promise.all([
this.lint(source),
this.test(source),
]);
console.log("Lint:", lintResult);
console.log("Test:", testResult);
// Build and push container image
const image = await this.build(source);
const digest = await image
.withRegistryAuth("ghcr.io", "github-actions", registryToken)
.publish("ghcr.io/org/app:latest");
return digest;
}
}
Services and Databases in Pipelines
@object()
class IntegrationTests {
@func()
async run(source: Directory): Promise<string> {
// Start a PostgreSQL service
const postgres = dag
.container()
.from("postgres:16")
.withEnvVariable("POSTGRES_PASSWORD", "test")
.withEnvVariable("POSTGRES_DB", "testdb")
.withExposedPort(5432)
.asService();
// Start a Redis service
const redis = dag
.container()
.from("redis:7")
.withExposedPort(6379)
.asService();
// Run tests with services attached
return dag
.container()
.from("node:20-slim")
.withDirectory("/app", source)
.withWorkdir("/app")
.withServiceBinding("postgres", postgres)
.withServiceBinding("redis", redis)
.withEnvVariable("DATABASE_URL", "postgres://postgres:test@postgres:5432/testdb")
.withEnvVariable("REDIS_URL", "redis://redis:6379")
.withExec(["npm", "ci"])
.withExec(["npm", "run", "test:integration"])
.stdout();
}
}
Running Locally and in CI
# Install Dagger CLI
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
# Run pipeline locally (same as CI)
dagger call ci --source=. --registry-token=env:GITHUB_TOKEN
# Run individual functions
dagger call lint --source=.
dagger call test --source=.
dagger call build --source=.
# Use in GitHub Actions
# .github/workflows/ci.yml
# - name: Run CI
# uses: dagger/dagger-for-github@v6
# with:
# verb: call
# args: ci --source=. --registry-token=env:GITHUB_TOKEN
Dagger Modules
// Use community modules from the Daggerverse
import { dag } from "@dagger.io/dagger";
@object()
class Deploy {
@func()
async deployToFly(source: Directory, flyToken: string): Promise<string> {
// Use the Fly.io module from Daggerverse
return dag
.fly()
.withToken(flyToken)
.deploy(source, { app: "my-app" });
}
@func()
async notifySlack(message: string, webhookUrl: string): Promise<void> {
await dag
.container()
.from("curlimages/curl")
.withExec([
"curl", "-X", "POST", webhookUrl,
"-H", "Content-Type: application/json",
"-d", JSON.stringify({ text: message }),
])
.sync();
}
}
Earthly — Reproducible Containerized Builds
Earthly combines the best of Dockerfiles and Makefiles — reproducible, containerized builds with caching that work identically everywhere.
Earthfile
# Earthfile — like Dockerfile + Makefile
VERSION 0.8
FROM node:20-slim
WORKDIR /app
# Shared dependency installation (cached)
deps:
COPY package.json package-lock.json ./
RUN npm ci
SAVE ARTIFACT node_modules
# Lint
lint:
FROM +deps
COPY . .
RUN npm run lint
# Unit tests
test:
FROM +deps
COPY . .
RUN npm run test
# Integration tests with services
integration-test:
FROM +deps
COPY . .
WITH DOCKER --compose docker-compose.test.yml
RUN npm run test:integration
END
# Build
build:
FROM +deps
COPY . .
RUN npm run build
SAVE ARTIFACT dist /dist
# Docker image
docker:
FROM node:20-slim
WORKDIR /app
COPY +deps/node_modules ./node_modules
COPY +build/dist ./dist
COPY package.json .
EXPOSE 3000
ENTRYPOINT ["node", "dist/server.js"]
SAVE IMAGE --push ghcr.io/org/app:latest
# Full CI pipeline
ci:
BUILD +lint
BUILD +test
BUILD +integration-test
BUILD +docker
# Multi-platform builds
docker-multiplatform:
BUILD --platform=linux/amd64 --platform=linux/arm64 +docker
Monorepo Support
# Earthfile — monorepo with shared dependencies
VERSION 0.8
# Root-level shared setup
FROM node:20-slim
WORKDIR /workspace
# Install root dependencies
root-deps:
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/shared/package.json ./packages/shared/
COPY packages/api/package.json ./packages/api/
COPY packages/web/package.json ./packages/web/
RUN npm install -g pnpm && pnpm install --frozen-lockfile
SAVE ARTIFACT node_modules
SAVE ARTIFACT packages/*/node_modules
# Build shared library first
shared-build:
FROM +root-deps
COPY packages/shared ./packages/shared
RUN cd packages/shared && pnpm run build
SAVE ARTIFACT packages/shared/dist
# API service
api-build:
FROM +root-deps
COPY +shared-build/dist ./packages/shared/dist
COPY packages/api ./packages/api
RUN cd packages/api && pnpm run build
SAVE ARTIFACT packages/api/dist
api-test:
FROM +root-deps
COPY +shared-build/dist ./packages/shared/dist
COPY packages/api ./packages/api
RUN cd packages/api && pnpm run test
api-docker:
FROM node:20-slim
COPY +api-build/dist /app/dist
COPY packages/api/package.json /app/
WORKDIR /app
RUN npm install --production
ENTRYPOINT ["node", "dist/server.js"]
SAVE IMAGE --push ghcr.io/org/api:latest
# Web frontend
web-build:
FROM +root-deps
COPY +shared-build/dist ./packages/shared/dist
COPY packages/web ./packages/web
RUN cd packages/web && pnpm run build
SAVE ARTIFACT packages/web/dist
# Build everything in parallel
all:
BUILD +api-test
BUILD +api-docker
BUILD +web-build
Earthly Satellites (Remote Caching)
# Install Earthly
curl -fsSL https://earthly.dev/get-earthly | sh
# Run locally
earthly +ci
# Run specific targets
earthly +lint
earthly +test
earthly +docker
# Remote caching with Earthly Satellites
earthly --ci --remote-cache=ghcr.io/org/earthly-cache +ci
# Use Earthly Satellite (remote runner with persistent cache)
earthly sat launch my-satellite
earthly --sat my-satellite +ci
# GitHub Actions
# - name: Build with Earthly
# run: earthly --ci --push +ci
Secrets and Conditional Builds
VERSION 0.8
# Secrets — never stored in cache layers
docker-with-secrets:
FROM +build
RUN --secret SENTRY_AUTH_TOKEN \
npx @sentry/cli releases new $VERSION && \
npx @sentry/cli releases files $VERSION upload-sourcemaps ./dist
# Conditional targets
deploy:
IF [ "$EARTHLY_CI" = "true" ]
BUILD +docker
RUN --secret FLY_API_TOKEN flyctl deploy
ELSE
RUN echo "Skipping deploy in local environment"
END
# Import from other Earthfiles
IMPORT github.com/earthly/lib/utils:3.0.2 AS utils
check:
DO utils+CHECK_TAG --tag=$TAG
Depot — Faster Docker Builds
Depot provides remote Docker build infrastructure — persistent caching, native multi-platform builders, and drop-in replacement for docker build.
Drop-In Replacement
# Install Depot CLI
curl -L https://depot.dev/install-cli.sh | sh
# Login
depot login
# Build — same syntax as docker build
depot build -t myapp:latest .
# Build and push
depot build -t ghcr.io/org/app:latest --push .
# Multi-platform build (native ARM + x86 builders)
depot build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/org/app:latest \
--push .
# Build with specific project
depot build --project my-project -t myapp:latest .
GitHub Actions Integration
# .github/workflows/build.yml
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: depot/setup-action@v1
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: depot/build-push-action@v1
with:
project: ${{ secrets.DEPOT_PROJECT_ID }}
push: true
tags: ghcr.io/${{ github.repository }}:latest
platforms: linux/amd64,linux/arm64
# Persistent cache — survives across builds
# No cache import/export needed
# Bake — multi-image builds
- uses: depot/bake-action@v1
with:
project: ${{ secrets.DEPOT_PROJECT_ID }}
push: true
files: docker-bake.hcl
Docker Bake (Multi-Image)
// docker-bake.hcl — build multiple images
group "default" {
targets = ["api", "web", "worker"]
}
target "api" {
context = "."
dockerfile = "packages/api/Dockerfile"
tags = ["ghcr.io/org/api:latest"]
platforms = ["linux/amd64", "linux/arm64"]
cache-from = ["type=registry,ref=ghcr.io/org/api:cache"]
cache-to = ["type=registry,ref=ghcr.io/org/api:cache,mode=max"]
}
target "web" {
context = "."
dockerfile = "packages/web/Dockerfile"
tags = ["ghcr.io/org/web:latest"]
platforms = ["linux/amd64", "linux/arm64"]
}
target "worker" {
context = "."
dockerfile = "packages/worker/Dockerfile"
tags = ["ghcr.io/org/worker:latest"]
platforms = ["linux/amd64"]
}
Programmatic API
// Depot API — manage projects and builds programmatically
const DEPOT_TOKEN = process.env.DEPOT_TOKEN!;
// List builds
const builds = await fetch("https://depot.dev/api/v1/builds", {
headers: { Authorization: `Bearer ${DEPOT_TOKEN}` },
});
const data = await builds.json();
for (const build of data.builds) {
console.log(`${build.id}: ${build.status} — ${build.duration}s`);
}
// Get build details
const build = await fetch(`https://depot.dev/api/v1/builds/${buildId}`, {
headers: { Authorization: `Bearer ${DEPOT_TOKEN}` },
});
// Trigger build via API
const newBuild = await fetch("https://depot.dev/api/v1/builds", {
method: "POST",
headers: {
Authorization: `Bearer ${DEPOT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
project_id: projectId,
dockerfile: "Dockerfile",
tags: ["ghcr.io/org/app:latest"],
platforms: ["linux/amd64", "linux/arm64"],
push: true,
}),
});
Feature Comparison
| Feature | Dagger | Earthly | Depot |
|---|---|---|---|
| Type | Programmable CI engine | Containerized build tool | Remote Docker builder |
| Pipeline Format | TypeScript/Python/Go code | Earthfile (Dockerfile-like) | Dockerfile (standard) |
| Run Locally | ✅ (identical to CI) | ✅ (identical to CI) | ✅ (depot build) |
| Container Native | ✅ (everything in containers) | ✅ (BuildKit-based) | ✅ (BuildKit-based) |
| Caching | Automatic (container layers) | Layer + shared caching | Persistent remote cache |
| Multi-Platform | ✅ (cross-compilation) | ✅ (SAVE IMAGE --platform) | ✅ (native ARM + x86) |
| Services in Pipeline | ✅ (asService) | ✅ (WITH DOCKER) | ❌ (build only) |
| Monorepo Support | ✅ (module composition) | ✅ (target dependencies) | ❌ (Dockerfile only) |
| Secrets Handling | ✅ (function parameters) | ✅ (--secret flag) | ✅ (Docker secrets) |
| Parallel Execution | ✅ (Promise.all) | ✅ (BUILD targets) | ✅ (bake groups) |
| CI Integration | Any CI (GitHub, GitLab, etc.) | Any CI | Any CI |
| Remote Execution | Dagger Cloud | Earthly Satellites | Depot runners |
| Module/Plugin System | Daggerverse modules | IMPORT from Git | ❌ |
| License | Apache 2.0 | BSL → Apache 2.0 | Proprietary |
| Learning Curve | Medium (SDK + containers) | Low (Dockerfile knowledge) | Very low (docker build) |
| Best For | Code-first CI/CD | Reproducible builds | Faster Docker builds |
When to Use Each
Choose Dagger if:
- You want to write CI/CD pipelines in TypeScript, Python, or Go
- Running pipelines locally before pushing to CI is important
- You need services (databases, caches) in your pipeline for integration tests
- The Daggerverse module ecosystem provides reusable pipeline components
- Portable pipelines that work on any CI platform matter
Choose Earthly if:
- Your team knows Dockerfiles and wants a similar syntax for builds
- Monorepo builds with cross-project dependencies are your use case
- Reproducible builds (same result everywhere) are a priority
- Shared caching with Earthly Satellites speeds up team builds
- Makefile-like target dependencies organize complex build graphs
Choose Depot if:
- You just need faster Docker builds with zero config changes
- Multi-platform builds (ARM + x86) without emulation are important
- Persistent build caching across CI runs eliminates cold starts
- Drop-in replacement for
docker buildin existing workflows - You don't need a full CI engine — just faster image builds
Methodology
Feature comparison based on Dagger v0.x (TypeScript SDK), Earthly v0.8+, and Depot documentation as of March 2026. Dagger evaluated on SDK expressiveness, service support, and Daggerverse. Earthly evaluated on Earthfile syntax, monorepo support, and caching. Depot evaluated on build speed, multi-platform support, and CI integration. Code examples use official CLIs and APIs.