Dagger CI CD vs GitHub Actions: Programmable, Container-Native Pipelines in 2026
Every team that has lived with a few hundred lines of workflow YAML knows the feeling. A pipeline fails on the runner, passes on the laptop, and the only way to debug it is to push another commit and wait four minutes. The logic of your build is smeared across shell snippets, marketplace actions, matrix expressions and vendor-specific caching keys, and none of it runs anywhere except the vendor’s cloud.
Dagger CI CD is one of the more serious answers to that problem. It moves pipeline logic out of YAML and into ordinary code, executes every step as a containerized function on a local or remote engine, and caches results by content. The vendor runner shrinks to a trigger. This article compares that model with GitHub Actions on mechanisms rather than slogans: how the engine works, what actually gets cached, where the two systems overlap, what breaks, and how to adopt Dagger without throwing away the Actions investment you already have.
What this covers: the architecture of a container-native engine, a working pipeline in code, how Dagger and Actions compose, a decision matrix, failure modes, and a migration plan.
Context and Background
GitHub Actions won the default-CI contest for a simple reason: it sits next to the code. Triggers, secrets, OIDC federation, environments, the marketplace and hosted runners arrive with the repository. For many teams, a twenty-line workflow is all the CI they will ever need, and nothing in this article argues otherwise.
The pain begins when pipelines grow. Workflow YAML is a configuration language pressed into service as a programming language. Conditionals become if: expressions in strings, reuse becomes composite actions and reusable workflows, and data passes between steps through files, outputs and environment variables. The unit of execution is a job on a VM, so any step that needs a specific toolchain either installs it every run or leans on a cache that is keyed by hand. If you run Actions at organizational scale, our guide to GitHub Actions at scale with OIDC, reusable workflows and Actions Runner Controller covers the governance side; here the focus is the programming model.
Dagger started from a different premise. It was created by Solomon Hykes and team, with the founding observation that a pipeline is a graph of containerized operations, and a graph of containerized operations is exactly what BuildKit already knows how to execute and cache. Early Dagger versions used CUE as the configuration language, and the project later pivoted to a GraphQL API with generated SDKs so that pipelines could be written in general-purpose languages. Today the engine exposes a typed API, and you drive it from Go, Python, TypeScript and several other SDKs. Per the release page checked for this article, the v0.21.11 release (21 October 2026) ships SDKs for TypeScript, Rust, Python, PHP, Go and Elixir alongside the CLI and a Helm chart. The documentation site now describes a 1.0-beta line and a migration guide, so expect API movement if you adopt before 1.0 stabilizes.
Dagger’s own docs position it as “third-generation CI, after Jenkins and GitHub Actions” and emphasize that artifacts are computed lazily as an incremental DAG of sandboxed functions, on a laptop or a shared remote engine (Dagger documentation). That is marketing language, but the technical claim underneath it is checkable: the same function call should behave the same on a laptop and on CI.
Two adjacent systems frame the comparison. Bazel solves hermeticity and remote execution for monorepos by demanding that you describe every input; see our piece on Bazel remote caching and hermetic builds. Dagger sits in a softer spot: it uses containers rather than a build graph of your source, so adoption is incremental. On the supply-chain side, whatever engine produces your artifacts must also emit provenance; the SLSA and Sigstore architecture guide explains what that evidence needs to contain.
One context note on cost, because it drives some of the 2026 interest in portable pipelines. GitHub announced a cut of up to 39 percent to GitHub-hosted runner prices effective 1 January 2026, and also announced a $0.002 per minute platform charge for self-hosted runner usage starting 1 March 2026. GitHub’s changelog now states that the self-hosted billing change is postponed for re-evaluation, with no new date given (GitHub changelog, 16 December 2025). Check the current status before budgeting. The point for this article is strategic: a pipeline whose logic lives in portable code is cheaper to move if any vendor’s pricing shifts.
The Container-Native Model: How Dagger Differs from Workflow YAML
Direct answer: Dagger runs your pipeline as a graph of typed functions, each executed in containers by a long-lived engine that caches every operation by content digest. GitHub Actions runs YAML-defined jobs on virtual machines. The practical difference is that a Dagger pipeline is callable from your laptop, from any CI vendor, and from tests, with identical behavior and shared caching.
The engine, the API and the session
The engine is a container image that bundles a BuildKit-derived solver, a GraphQL API server and the plumbing for secrets, services and telemetry. You do not talk to it directly. The dagger CLI, or an SDK, opens a session against the engine, which starts one if none is reachable, and sends GraphQL queries. Because the API is typed, the SDKs are generated from the schema, which is why a Go, Python and TypeScript pipeline all have the same shape: dag.Container().From(...).WithExec(...).

Figure 1: The Dagger CI CD architecture. Clients on a laptop or any CI runner talk to one engine through a GraphQL session; the engine builds a lazy DAG, runs sandboxed containers, caches by content and emits OpenTelemetry traces.
Figure 1 shows why the runner becomes interchangeable. The laptop and the CI runner are both just clients. Nothing about the pipeline’s semantics depends on which one you use, provided both can reach an engine, and provided you accept that an engine is itself a stateful service you must size and operate.
Laziness and the DAG
The key semantic is laziness. When your code writes ctr = dag.container().from_("golang:1.23").with_exec(["go", "test", "./..."]), nothing executes. You have built a query. Execution happens only when you ask for a leaf value, such as .stdout(), .sync(), a file export or a registry push. At that point the engine resolves the query into a DAG of operations and runs only those that contribute to the requested result.
This has three consequences worth stating exactly. First, you can compose pipelines freely in code, passing half-built containers between functions, without paying for intermediate execution. Second, independent branches of the DAG run in parallel without you declaring needs: edges, because the graph is derived from data flow, not from job ordering. Third, an operation that is not an ancestor of the requested output never runs, which is the reverse of YAML, where a job runs unless a condition skips it.
Content-addressed caching
Each operation in the DAG is identified by a digest computed from its inputs: the parent layer digest, the command, environment, mounted directories and secrets references. If the engine has seen the same digest, it returns the stored result. This is the BuildKit caching model applied to the whole pipeline instead of only docker build.
Compare that with Actions. actions/cache stores an archive under a key you design, restores it at the start of a job and saves it at the end. The key design, restore keys and invalidation are manual, and the unit is a tarball of a directory. In Dagger, a cache hit is automatic for deterministic operations, and you add explicit cache volumes (dag.CacheVolume("go-mod")) for the things that are not content-addressable, such as package manager download directories. The two are not equivalent in effect, and I will come back to where Dagger’s cache is weaker in the gotchas section.
Functions, modules and the Daggerverse
Since the 0.10 era, the unit of reuse is the module: a package of typed functions with a declared interface, written in any supported SDK language and callable from the CLI or from other modules. A function accepts typed arguments such as Directory, File, Secret, Container or Service, and returns typed values. The CLI exposes functions as subcommands automatically, so dagger call test --source=. works with no glue.
Modules can call other modules across languages, since the boundary is the GraphQL schema. A public registry called the Daggerverse indexes community modules. Treat the Daggerverse the way you treat the Actions marketplace: useful for discovery, but a supply-chain surface. Pin versions by commit and review what you consume.
Services and secrets as first-class types
Integration tests need a database. In Actions, you declare services: on a job, and the runner starts sibling containers on a job network. In Dagger, a Service is a value: you build a container, call .as_service(), and bind it into the test container with with_service_binding("db", svc). The engine starts the service when the dependent step runs and tears it down afterward, and the service is addressable by hostname inside the sandbox. Because it is just a value in your code, you can parameterize it and reuse it in a different function.
Secrets follow the same idea. A Secret is a handle backed by an environment variable, a file, a command or an external provider; the plaintext is mounted into the exec at run time and is not written to the cache or to logs. In Actions, secrets are injected as environment variables into the job and masked in logs. The Dagger approach narrows exposure to the single step that mounts the secret.
Walk-through: A Real Pipeline in Code
The best way to see the difference is to write the same pipeline both ways. The example is a Python service with lint, unit tests, an integration test against Postgres, a container build and a gated publish. The code below uses the Python SDK; the Go and TypeScript versions are structurally identical. Treat the snippets as a teaching sketch and check method names against the SDK reference for the version you pin, since the API is moving toward 1.0.
The module
import dagger
from dagger import dag, function, object_type
@object_type
class Ci:
def _base(self, source: dagger.Directory) -> dagger.Container:
pip_cache = dag.cache_volume("pip-cache")
return (
dag.container()
.from_("python:3.12-slim")
.with_mounted_cache("/root/.cache/pip", pip_cache)
.with_directory("/src", source, exclude=[".git", ".venv"])
.with_workdir("/src")
.with_exec(["pip", "install", "-r", "requirements.txt"])
)
@function
async def lint(self, source: dagger.Directory) -> str:
return await self._base(source).with_exec(["ruff", "check", "."]).stdout()
@function
async def test(self, source: dagger.Directory) -> str:
db = (
dag.container()
.from_("postgres:16")
.with_env_variable("POSTGRES_PASSWORD", "test")
.with_exposed_port(5432)
.as_service(use_entrypoint=True)
)
return await (
self._base(source)
.with_service_binding("db", db)
.with_env_variable("DATABASE_URL", "postgresql://postgres:test@db:5432/postgres")
.with_exec(["pytest", "-q"])
.stdout()
)
@function
def build(self, source: dagger.Directory) -> dagger.Container:
return (
dag.container()
.from_("python:3.12-slim")
.with_directory("/app", source, exclude=[".git", "tests"])
.with_workdir("/app")
.with_exec(["pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
.with_entrypoint(["python", "-m", "service"])
)
@function
async def publish(
self, source: dagger.Directory, registry_token: dagger.Secret, ref: str
) -> str:
await self.lint(source)
await self.test(source)
return await (
self.build(source)
.with_registry_auth("ghcr.io", "ci-bot", registry_token)
.publish(ref)
)
Run it locally with dagger call test --source=. and CI runs the identical call. The publish function awaits lint and test before building, so the gate is ordinary control flow. Compare that with expressing “publish only if lint and test passed, and only on main, and with this secret” through needs:, if: and environment protection rules.

Figure 2: The pipeline DAG. One Source directory fans out to lint, test and build functions; the gate waits for all of them; publish runs only when the gate opens.
Figure 2 shows that DAG. The fan-out from Source is not declared anywhere. It emerges because lint, test and build each take the same Directory and have no data dependency on each other. When you call publish, the awaits above are sequential in this sketch; if you want parallelism between lint and test, run them concurrently with asyncio.gather. The engine will already dedupe the shared _base layers, since both derive from the same digest.
Wiring it into GitHub Actions
You do not have to leave Actions. The pragmatic pattern is to keep triggers, permissions and OIDC in the workflow and delegate the logic to Dagger:
name: ci
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Dagger CLI
run: curl -fsSL https://dl.dagger.io/dagger/install.sh | DAGGER_VERSION=0.21.11 sh
- name: Test
run: ./bin/dagger call test --source=.
- name: Publish
if: github.ref == 'refs/heads/main'
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: >
./bin/dagger call publish --source=. --registry-token=env:TOKEN
--ref=ghcr.io/acme/service:${{ github.sha }}
The install line follows the pattern in Dagger’s install documentation; pin a version and verify the script URL and the binary location against the current docs before copying. Dagger also maintains an official GitHub Action; either approach works, and the curl form makes the dependency visible.
Notice what the YAML no longer contains: no language setup, no cache steps, no service containers, no matrix of toolchains. The workflow is a thin adapter. If a vendor changes pricing or you move to another CI system, you rewrite twenty lines, not two hundred.
What happens on a push

Figure 3: The push sequence. Trigger, connect, digest lookup, execute only what is missing, return an exit code.
The sequence in Figure 3 shows the central behavior. On a pull request that only touched documentation, the engine computes digests for the lint and test operations, finds that none of the relevant inputs changed, and returns the stored results. The job finishes in the time it takes to start the CLI and connect. On a fresh runner with an empty engine, everything is a miss and the cost looks like any other CI run. This is the crucial caveat: Dagger’s speed advantage depends on engine cache persistence, not on Dagger itself.
Where the engine lives determines the performance
With ephemeral hosted runners, each job starts a new engine with an empty cache. You then have three options.
- Accept cold starts. Simple, and often fine for small repos. Layer caching inside one job still helps, since lint, test and build share a base.
- Export and import the cache. Dagger supports configuring remote cache storage so engines can share layers through a registry or object store. This restores some warmth but costs transfer time per job. Verify the exact configuration keys in the docs for your version.
- Run a persistent engine. Self-hosted runners, or a dedicated engine host the runners connect to, keep the cache warm across jobs. This is where the largest wins come from and also where operations work begins: disk sizing, pruning policy, upgrades, and isolation between untrusted pull requests and trusted branches.
Dagger’s docs also describe a managed offering, Dagger Cloud, for traces and shared visibility. As of this writing the docs link to cloud checks and self-hosting guides; I could not verify current plan limits or pricing from the pages I fetched, so check dagger.io before making cost claims in your own business case.
Observability: the trace is the log
Every function call becomes a span. The CLI renders these in a terminal UI as a tree, with cached steps visibly marked, and the same spans export over OpenTelemetry. For a debugging workflow this is a real difference. In Actions you read a flat log per step. In Dagger you can see that pip install was a cache hit, that Postgres started at a given second, and that the failing pytest ran in a particular container, then re-run the exact function locally. v0.21.10 added opt-in engine CPU and memory telemetry from cgroup v2, enabled through telemetry.resourceMetrics in the engine configuration and disabled by default, which matters when you are sizing a shared engine.
Dagger vs GitHub Actions: A Direct Comparison
The honest comparison is not a feature checklist, because the two tools overlap only partially. Actions is a scheduler, trigger system, secret store, runner fleet and workflow language. Dagger is an execution engine and programming model. Most real adoption puts Dagger inside Actions, not instead of it. Where they genuinely compete is in the area of pipeline logic and portability.

Figure 4: A decision flow for CI as code. Simple pipelines stay as YAML; complex or multi-repo logic that must be reproducible locally moves into Dagger functions; very large monorepos should evaluate Bazel.
Decision matrix
| Dimension | GitHub Actions (YAML) | Dagger (functions) |
|---|---|---|
| Pipeline language | YAML plus expressions and shell | Go, Python, TypeScript, others |
| Unit of execution | Job on a VM | Containerized function on an engine |
| Local reproduction | Partial, via third-party emulators | Native: same dagger call locally |
| Caching | Explicit keys, tarball restore | Content-addressed per operation, plus cache volumes |
| Parallelism | Declared with needs: and matrix |
Derived from data flow in the DAG |
| Triggers, events, schedules | First class | None; delegates to the host CI |
| Secrets and OIDC | Built in, with environment protection | Secret type, but relies on host for identity |
| Reuse | Marketplace, composite actions, reusable workflows | Typed modules, Daggerverse |
| Testing the pipeline | Hard | Unit-testable like code |
| Debugging | Flat logs per step | Span tree, cached steps visible, local re-run |
| Operational burden | Low for hosted runners | Engine to size, prune and upgrade |
| Maturity | Very mature, huge ecosystem | Pre-1.0 line, API changes still occurring |
| Vendor lock-in | High for workflow syntax | Low for logic; host adapter remains |
Where Dagger is genuinely better
Reproducibility. The ability to run the whole pipeline, not a script that approximates it, on a laptop changes the debugging loop from minutes to seconds. The difference is not about speed alone. A developer who can run CI locally tends to run it before pushing, so fewer broken commits reach the shared queue.
Logic. Loops, functions, types, error handling and unit tests exist in a real language. A pipeline that builds fifteen services with slightly different flags is a for loop and a struct, not a matrix with fifteen includes.
Cross-vendor portability. An organization with GitHub Actions for open source, GitLab for an acquired team and Jenkins for a legacy product can share a single module library. Each vendor file becomes a thin adapter.
Agent and automation workflows. The Dagger docs frame the system as a way to run the same checks on code from coding agents as on handwritten code, with typed artifacts and interactive inspection. Whether that matters to you depends on how much machine-generated code reaches your repositories. Treat that framing as the vendor’s positioning; the underlying capability, a reproducible check callable by anything that can run a CLI, is simply useful.
Where Actions is still better
Triggers and platform integration. Pull request events, check runs, environments with required reviewers, deployment protections, dependency review and code scanning are platform features. Dagger does not replace them and does not try to.
Zero-ops start. A hosted runner needs no engine. For a small team, the engine adds a thing to monitor.
Ecosystem breadth for the platform itself. Some tasks, such as labeling issues or managing releases, are naturally event handlers. Writing them as Dagger functions gains little.
Familiarity. Everybody on your team can read a workflow file. Fewer can read an unfamiliar SDK’s fluent API on day one, and that cost is real.
Dagger and Bazel are not the same bet
Readers coming from build-system backgrounds sometimes ask whether Dagger is a Bazel competitor. It is not quite. Bazel requires you to declare sources and dependencies precisely, enabling fine-grained invalidation and remote execution at monorepo scale. Dagger’s invalidation granularity is the container operation: if you copy the whole source directory into a layer and run tests, any file change invalidates that layer. You can reduce the damage by copying dependency manifests first and the rest later, a technique familiar from Dockerfiles, but you cannot reach Bazel’s per-target precision without describing targets. If your repository has thousands of targets and compile times dominate, read the Bazel remote caching guide linked in Further Reading before choosing. If your pain is glue logic and reproducibility, Dagger is closer to the problem.
Deeper Mechanics: Caching, Secrets and Isolation
What invalidates a Dagger cache entry
Cache correctness depends on you telling the engine what the inputs are. These are the practical rules.
- Directory inputs are hashed by content. Passing
sourceinto a container layer means any changed file invalidates every downstream operation. Use theexcludeandincludefilters, and.daggerignore-style configuration where available, to keep noise such as.gitand build output out of the hash. - Commands are opaque.
with_exec(["pip", "install", "-r", "requirements.txt"])is cached as long as its inputs are identical, even if PyPI would serve different bytes tomorrow. The cache cannot know about network changes. Pin versions and hashes in lock files. - Environment variables are inputs. Setting a changing variable early in the chain, such as a timestamp or commit SHA, invalidates everything after it. Inject volatile values at the last step.
- Cache volumes are not content-addressed. They persist mutable state, which is useful for package managers and unsafe if a tool behaves differently depending on stale contents. Treat them as an optimization and be ready to purge them.
- Opting out. Functions can be marked not cacheable when they have side effects, such as a deployment. Verify the exact annotation for your SDK version. Without it, a deployment call with identical inputs could be served from cache and silently skipped, which is one of the surprising behaviors new users hit.
A worked example makes the stakes concrete. Suppose a service has 400 source files and a lockfile. A naive pipeline copies everything, installs dependencies and runs tests. Any edit to any file invalidates the dependency install, so each run reinstalls. The restructured pipeline copies only the lockfile, runs the install, then copies the source. Now source edits invalidate only the layers after the install. Whether this saves seconds or minutes depends on the dependency tree. The point is the mechanism, and I am deliberately not offering a benchmark: results depend on your repo, language, network and engine placement, and any published figure from a different environment would mislead you.
Secrets without leaking them
Dagger’s Secret type supports sources such as env:NAME, file:path and cmd:, and others via providers; check the current docs for the supported schemes. Two properties matter. The secret value does not participate in the cache key as plaintext, so a rotated secret does not by itself invalidate steps that only consume it as a mount, and a leaked cache does not contain it, provided the step does not write it into a layer. The second property is the catch: if your command writes the secret into a file inside the image, the engine cannot save you. Always check that secrets are mounted, not copied.
A harder question is identity. Actions can mint short-lived cloud credentials through OIDC federation to AWS, Azure or Google Cloud, with no stored keys. When Dagger runs inside an Actions job, you can keep that arrangement: the workflow obtains the token, and your function receives it as a Secret. That is the cleanest division of labor, and it keeps the long-lived credential problem solved where the platform already solves it. The same logic applies to supply-chain attestations. If you need signed provenance, the signing identity comes from the CI platform’s OIDC token, so your Dagger function should output the artifact digest and let a platform step attest to it, a flow the SLSA and Sigstore guide walks through in detail.
Isolation and the shared-engine problem
A persistent shared engine is a performance asset and a security liability. Two risks deserve explicit design.
Untrusted code. A pull request from a fork runs arbitrary code in your pipeline. In a container-native engine, the sandbox is a container on a shared host, with a shared cache. Cache poisoning, where an untrusted job writes a layer that a trusted job later reuses, is the scenario to reason about. Because cache entries are keyed by input digest, a poisoned entry would require the attacker to produce an operation with the same digest as a trusted operation but a different result, which content addressing is designed to prevent for deterministic operations. Cache volumes, being mutable, do not have that protection. Use separate engines, or at minimum separate cache volume names, for untrusted and trusted workflows.
Privileged execution. The engine commonly needs elevated privileges on its host to run containers. Running it on a shared self-hosted runner that also hosts other workloads widens the blast radius. Dedicated hosts or isolated node pools are a sound default, a pattern consistent with the runner-isolation guidance in our Actions-at-scale piece.
Cost model, with illustrative arithmetic
I will use clearly illustrative numbers to show the shape of the trade, not measured results. Assume a team runs 200 pipeline executions a day, each taking 12 minutes on a hosted runner billed per minute. That is 2,400 runner minutes a day. If a warm persistent engine allowed the typical run to complete in 5 minutes because most operations were cache hits, runner minutes fall to 1,000. The saving is real only if the cost of the engine host, its operation and the lost isolation are lower than the saved runner minutes, and only if the 5-minute figure is true for your workloads. For many teams the larger gain is developer time spent not waiting, which is hard to price and easy to overstate. Measure on your own repositories with a two-week trial before committing.
Trade-offs, Gotchas, and What Goes Wrong
The engine is infrastructure. Teams adopt Dagger for developer experience and discover they own a stateful service. Disk fills up with layers, pruning policies evict warm entries at bad moments, and an engine upgrade changes cache behavior. The v0.21.11 release notes, for example, fix retention of schema servers from ended sessions that could eventually cause out-of-memory kills on long-running engines. That is the sort of bug you only meet on long-lived engines, which is exactly the setup that gives the best performance. Budget operations time and monitor memory.
Pre-1.0 churn. The docs site presents a 1.0-beta line and a migration guide from earlier versions. If you pin and test upgrades, this is manageable; if you float versions across a fleet, it is how pipelines break on a Tuesday. Pin the CLI, the engine and the SDK to the same version, because the release notes show they move together.
You still need the host CI. Dagger does not schedule, trigger or authenticate you to the code host. Teams that expected to delete their workflow files are disappointed. The honest framing is that you are shrinking workflows, not eliminating them.
Language and tooling choices become pipeline dependencies. Writing CI in Go means CI maintainers need Go. Writing it in the application’s language lowers friction for developers but raises the bar for platform engineers who support many languages. Pick one SDK for the platform team and resist the temptation to let every service choose its own.
Debugging is better but different. The trace tree and local reproduction are real improvements, yet new failure modes appear: engine connectivity, version skew between CLI and engine, nested container permissions, and DNS or proxy configuration inside sandboxes on locked-down networks. Plan an internal runbook.
Over-abstraction. Because pipelines are code, you can build an elaborate framework around them. Resist this. A pipeline that needs a custom DSL to read is YAML with extra steps. The test I use: can a developer who did not write it explain a failing run after reading one function?
Cache lies. Content addressing gives correct results for pure operations and incorrect ones for impure operations that look pure. A step that downloads the “latest” tool will be served stale from cache and nobody will notice for weeks. Pin everything and add an explicit weekly cold build to flush surprises.
Not a deployment system. Dagger can run kubectl apply or a Terraform step, but it does not provide approvals, environments, rollbacks or drift detection. Use it to produce and verify artifacts, and let a deployment layer handle promotion.
Vendor and project risk. Dagger is a commercial company’s open source project; the engine is open source, while the cloud features are a product. As with any younger tool, evaluate roadmap, maintainer activity and your exit path. The good news is that exit is cheap relative to YAML lock-in, because your logic is in code and the underlying primitives are containers.
Practical Recommendations
Start where the pain is, not where the novelty is. If your workflows are short and boring, do not adopt Dagger; the engine’s operational cost will exceed the benefit. If you have one or more of these symptoms, a pilot is justified: pipelines that cannot be reproduced locally, copy-pasted workflow logic across many repositories, a polyglot CI estate across several vendors, or a build whose repeated dependency installation dominates runtime.
Pilot on one repository with a real pipeline, not a toy. Keep the Actions workflow as the trigger and call Dagger functions from it. Measure three numbers before and after: median and 95th percentile pipeline duration, the share of pull requests that fail CI after a clean local run, and engine operating effort in hours. If the second number does not move, local reproduction is not delivering value in your team.
Decide engine placement early. For a pilot, ephemeral engines on hosted runners are fine and show you the cold-start cost honestly. If the numbers justify it, move to persistent engines on dedicated runners, separated for trusted and untrusted workflows.
A short adoption checklist:
- Pick one SDK language for the platform team and one repository for the pilot.
- Pin the Dagger CLI, engine and SDK to one version, and schedule upgrade tests.
- Keep triggers, OIDC and environment protection in the host CI; pass credentials to functions as
Secretvalues. - Restructure functions so lockfiles are copied before source, and keep volatile values out of early layers.
- Mark side-effecting functions as non-cacheable and verify they actually run.
- Separate caches and engines for forks and trusted branches.
- Export traces to your observability stack and set an alert on engine disk and memory.
- Review third-party modules the way you review dependencies, pinning to a commit.
- Add a weekly cold build to catch hidden cache dependence.
- Re-evaluate after 60 days against the metrics above.
If you are already running Actions at scale, treat Dagger as a complement to the governance patterns, reusable workflows and runner controllers you have built, not a replacement. And if your repository is a large monorepo where compile graph precision matters, compare against Bazel before investing.
Frequently Asked Questions
Is Dagger a replacement for GitHub Actions?
Mostly no. Dagger replaces the pipeline logic inside your jobs, not the trigger system, secrets store, runner fleet or code-host integration that Actions supplies. The usual architecture keeps a short workflow that checks out code, installs the Dagger CLI and calls a function. Some teams do run Dagger under other CI systems or plain cron, but for GitHub-hosted repositories the two are complementary far more often than competing.
Do I need Docker to run Dagger?
Dagger needs a container runtime to host its engine, and Docker is the commonly used one; the CLI starts the engine as a container if none is available. Documentation also covers connecting to a remote engine, such as one on a dedicated host or in Kubernetes, so a runner does not have to host the engine itself. Check the install page for supported runtimes and privilege requirements for your version.
How does Dagger caching compare with actions/cache?
Actions caching is explicit: you choose a key, the runner restores a tarball and saves it afterward. Dagger caches each operation automatically by input digest, so unchanged steps return stored results, and it adds cache volumes for mutable stores like package directories. The catch is that ephemeral runners start with an empty engine, so the benefit depends on cache persistence or remote cache sharing.
Which languages can I write Dagger pipelines in?
The release page for v0.21.11 lists SDKs for TypeScript, Rust, Python, PHP, Go and Elixir alongside the CLI, with Go, Python and TypeScript the most commonly documented. Because modules communicate through a typed GraphQL schema, a module written in one language can be consumed from another. Choose the language your platform team already maintains, and confirm SDK maturity for your pick before committing.
Is Dagger production ready in 2026?
Teams use it in production, and the project ships frequent patch releases, with v0.21.11 dated 21 October 2026. The docs, however, now show a 1.0-beta line and a migration guide, which signals that interfaces are still settling. Production readiness is therefore a matter of discipline: pin versions, test upgrades, monitor the engine, and keep the host CI adapter thin so you can roll back.
Will Dagger make my CI cheaper?
Possibly, but it is not automatic. Savings come from cache hits that shorten runs, fewer wasted pushes thanks to local reproduction, and less duplicated logic. Costs include operating an engine and possibly cloud features. GitHub’s runner pricing changed in 2026, with hosted prices cut by up to 39 percent and a self-hosted charge postponed, so measure your own numbers rather than assuming either direction.
Further Reading
- GitHub Actions at scale: OIDC federation, reusable workflows and Actions Runner Controller for the governance and security layer that Dagger functions run inside.
- Bazel remote caching and hermetic builds for monorepos for the alternative when target-level precision matters.
- SLSA and Sigstore software supply chain security architecture for provenance and signing of the artifacts your pipeline produces.
- Dagger documentation and the Dagger releases page for current versions and SDK references.
- GitHub changelog on Actions pricing for the 2026 pricing status.
By Riju — about
