Skip to main content

Dagger vs Earthly vs Depot: Programmable CI/CD Engines Compared (2026)

·PkgPulse Team

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

FeatureDaggerEarthlyDepot
TypeProgrammable CI engineContainerized build toolRemote Docker builder
Pipeline FormatTypeScript/Python/Go codeEarthfile (Dockerfile-like)Dockerfile (standard)
Run Locally✅ (identical to CI)✅ (identical to CI)✅ (depot build)
Container Native✅ (everything in containers)✅ (BuildKit-based)✅ (BuildKit-based)
CachingAutomatic (container layers)Layer + shared cachingPersistent 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 IntegrationAny CI (GitHub, GitLab, etc.)Any CIAny CI
Remote ExecutionDagger CloudEarthly SatellitesDepot runners
Module/Plugin SystemDaggerverse modulesIMPORT from Git
LicenseApache 2.0BSL → Apache 2.0Proprietary
Learning CurveMedium (SDK + containers)Low (Dockerfile knowledge)Very low (docker build)
Best ForCode-first CI/CDReproducible buildsFaster 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 build in 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.