Earthly Cloud shut down on July 16, 2025. The open-source CLI entered maintenance mode (critical bug fixes only, no new features), and Earthly Satellites, the remote caching runners, are gone. If you’re still running Earthfiles, now is the time to migrate.
Dagger is the most natural replacement. Like Earthly, it runs every build step in containers for reproducibility. Unlike Earthly, your pipeline logic is written in a real programming language (Python, Go, or TypeScript) instead of a DSL. This guide maps every Earthly concept to its Dagger equivalent with side-by-side code examples, then walks through converting a real multi-target Earthfile.
Tested April 2026 | Dagger v0.20.3, Python SDK
What Happened to Earthly
Earthly CI (the full platform) shut down in October 2023 after failing to convert adoption into revenue. The company pivoted to Earthly Satellites (remote BuildKit runners with persistent caching), but that also couldn’t sustain the business. On July 16, 2025, Earthly Cloud and Satellites were discontinued. The company pivoted again to Earthly Lunar, an AI guardrails product.
The earthly/earthly GitHub repo is still public but in maintenance mode. No new PRs are accepted. The documentation at docs.earthly.dev remains accessible for now, but there’s no guarantee for how long. If you’re depending on Earthly features like Satellites or remote caching, those are already gone.
Why Dagger (and Not Something Else)
Several tools overlap with what Earthly did, but Dagger is the closest fit for three reasons:
- Container-native execution: like Earthly, every Dagger function runs in its own container. Nothing leaks from the host. Builds are reproducible across environments
- Content-addressed caching: Dagger’s engine caches by content hash, similar to how Earthly cached build layers. Unchanged steps are skipped automatically
- CI portability: Earthly worked with any CI system. Dagger does too. Same pipeline code runs locally, in GitHub Actions, GitLab CI, and Jenkins
Dagger also offers a free one-year Cloud Team tier for former Earthly customers. Contact [email protected] with proof of Earthly usage to get the promo code.
Concept Mapping: Earthly to Dagger
Every Earthly concept has a Dagger equivalent. The syntax is different (DSL vs. programming language), but the mental model transfers directly.
| Earthly | Dagger (Python SDK) | Notes |
|---|---|---|
FROM image | dag.container().from_("image") | Start from a base image |
RUN command | .with_exec(["cmd", "arg"]) | Run a command. Uses exec-style args, not shell strings |
COPY +target/file . | .with_file("/dest", source.file("path")) | Copy files between stages |
WORKDIR /path | .with_workdir("/path") | Set working directory |
ENV KEY=value | .with_env_variable("KEY", "value") | Set environment variables |
ARG name=default | Function parameter with default | def build(self, tag: str = "latest") |
SAVE ARTIFACT ./app | Return dagger.File | Export with .export("/local/path") |
SAVE IMAGE tag | .publish("registry/image:tag") | Push to registry |
BUILD +target | Call another function | await self.build(source) |
| Earthfile targets | Dagger functions | Each @function = one build target |
WITH DOCKER | Dagger Services | container.as_service() + .with_service_binding() |
| Layer caching | Automatic + with_mounted_cache() | Content-addressed, no manual keys |
Example Migration: Multi-Target Earthfile
Here’s a typical Earthfile for a Go application with separate build, test, and deploy targets:
Original Earthfile
VERSION 0.8
FROM golang:1.22-alpine
WORKDIR /app
deps:
COPY go.mod go.sum .
RUN go mod download
SAVE ARTIFACT go.mod AS LOCAL go.mod
SAVE ARTIFACT go.sum AS LOCAL go.sum
build:
FROM +deps
COPY . .
RUN go build -o /usr/local/bin/myapp .
SAVE ARTIFACT /usr/local/bin/myapp
test:
FROM +deps
COPY . .
RUN go test -v ./...
lint:
FROM +deps
COPY . .
RUN go vet ./...
docker:
FROM alpine:latest
COPY +build/myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]
EXPOSE 8080
ARG tag=latest
SAVE IMAGE myregistry/myapp:${tag}
all:
BUILD +test
BUILD +lint
BUILD +docker
Migrated to Dagger (Python SDK)
First, set up the Dagger module in your project (see the installation guide if Dagger is not yet installed):
dagger init --name=myapp --sdk=python
Open the pipeline file and translate each Earthfile target into a Dagger function:
vi .dagger/src/myapp/main.py
Replace the contents with:
import dagger
from dagger import dag, function, object_type
@object_type
class Myapp:
@function
def deps(self, source: dagger.Directory) -> dagger.Container:
"""Install Go dependencies (equivalent to +deps target)"""
return (
dag.container()
.from_("golang:1.22-alpine")
.with_workdir("/app")
.with_file("/app/go.mod", source.file("go.mod"))
.with_file("/app/go.sum", source.file("go.sum"))
.with_exec(["go", "mod", "download"])
)
@function
async def build(self, source: dagger.Directory) -> dagger.File:
"""Build the Go binary (equivalent to +build target)"""
return (
self.deps(source)
.with_directory("/app", source)
.with_exec(["go", "build", "-o", "/usr/local/bin/myapp", "."])
.file("/usr/local/bin/myapp")
)
@function
async def test(self, source: dagger.Directory) -> str:
"""Run tests (equivalent to +test target)"""
return await (
self.deps(source)
.with_directory("/app", source)
.with_exec(["go", "test", "-v", "./..."])
.stdout()
)
@function
async def lint(self, source: dagger.Directory) -> str:
"""Run linter (equivalent to +lint target)"""
return await (
self.deps(source)
.with_directory("/app", source)
.with_exec(["go", "vet", "./..."])
.stdout()
)
@function
async def publish(self, source: dagger.Directory, tag: str = "latest") -> str:
"""Build and push container image (equivalent to +docker target)"""
binary = await self.build(source)
return await (
dag.container()
.from_("alpine:latest")
.with_file("/usr/local/bin/myapp", binary)
.with_entrypoint(["/usr/local/bin/myapp"])
.with_exposed_port(8080)
.publish(f"myregistry/myapp:{tag}")
)
@function
async def all(self, source: dagger.Directory) -> str:
"""Run tests, lint, and build (equivalent to +all target)"""
await self.test(source)
await self.lint(source)
return await self.publish(source)
What Changed
- Targets became functions: each Earthfile target (
+deps,+build,+test) maps to a@functionmethod - ARG became function parameters:
ARG tag=latestbecomestag: str = "latest"with type safety - COPY between targets became function calls: instead of
COPY +build/myapp, thepublishfunction callsself.build(source)and gets the file object directly - SAVE IMAGE became publish():
.publish("registry/image:tag")pushes to a registry and returns the image reference with digest - BUILD +target became function calls: the
all()function calls other functions sequentially. The Dagger Engine automatically parallels independent operations within each function
Running the Migrated Pipeline
Where you previously ran earthly +test, now run:
dagger call test --source=.
Where you ran earthly +docker --tag=v2.0:
dagger call publish --source=. --tag=v2.0
The full pipeline (earthly +all):
dagger call all --source=.
Updating CI Configuration
Your CI files need a one-line change. Replace earthly +target with dagger call function --source=..
For GitHub Actions, swap the Earthly action for Dagger’s:
# Before (Earthly)
- uses: earthly/actions-setup@v1
- run: earthly +test
# After (Dagger)
- uses: dagger/dagger-for-github@v8
with:
verb: call
args: test --source=.
For GitLab CI:
# Before (Earthly)
script:
- earthly +test
# After (Dagger)
before_script:
- curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh
script:
- dagger call test --source=.
For a complete GitHub Actions integration guide with secrets and caching, see our dedicated article.
Replacing Earthly Satellites (Remote Caching)
Earthly Satellites were remote BuildKit runners with persistent caching. They kept the cache warm between CI runs, which made builds significantly faster on ephemeral runners. That service is gone.
Dagger Cloud offers similar functionality. The $50/mo Team plan distributes your build cache across 26 global regions. Ephemeral CI runners (like GitHub Actions’ standard runners) get cache hits because the cache lives in Dagger Cloud, not on the runner. The free tier provides pipeline visualization and traces for a single user.
For self-hosted runners, Dagger’s cache persists automatically in Docker volumes between jobs. No cloud service needed.
What You Gain (and Lose) in the Migration
| Area | Earthly | Dagger |
|---|---|---|
| Pipeline language | Earthfile DSL (Dockerfile-like) | Python, Go, TypeScript (real code) |
| Learning curve | Low (if you know Docker) | Medium (requires SDK knowledge) |
| Type safety | None (string-based) | Full type checking with editor support |
| Local debugging | Run locally with earthly +target | Run locally with dagger call, plus interactive breakpoints |
| Remote caching | Satellites (discontinued) | Dagger Cloud (active, $50/mo) |
| IDE support | Syntax highlighting only | Full autocompletion, refactoring, inline docs |
| Module ecosystem | Cross-repo imports | Daggerverse (1,500+ modules) |
| Active development | Maintenance mode | Very active (v0.20.3, March 2026) |
The biggest loss is Earthly’s low barrier to entry. If your team could write Dockerfiles, they could write Earthfiles in minutes. Dagger requires learning a programming SDK. The biggest gain is real programming language support with full IDE tooling, type safety, and the ability to write complex logic (conditionals, loops, error handling) without wrestling a DSL.
Migration Checklist
- Install Dagger CLI and Docker on your development machine
- Run
dagger init --sdk=python(or go/typescript) in your project - Map each Earthfile target to a Dagger function using the concept table above
- Test locally with
dagger call <function> --source=. - Update CI configuration to call
dagger callinstead ofearthly - Set up Dagger Cloud for remote caching (replaces Satellites)
- Remove the Earthfile and
.earthly/directory once Dagger pipeline is verified - Contact
[email protected]for the free one-year Cloud Team tier (Earthly customer offer)
Start with your simplest Earthfile target, verify it works in Dagger, then migrate the rest incrementally. There’s no need to convert everything at once.