Skip to content

Dagger Footguns


1. Mounting the Full Source Directory Kills Layer Caching

You mount the entire project directory at the start of the pipeline with WithMountedDirectory("/src", source). Every time any file changes — including unrelated files like README or local config — the cache invalidates and all subsequent steps re-run from scratch. Fix: Mount only what each step needs. Copy go.sum/package-lock.json first, run the dependency install, then mount full source. This way dependency installs only re-run when dependency files change.


2. Passing Secrets as Plain String Arguments

You pass a token as a string argument: dagger call deploy --token=$GITHUB_TOKEN. The token appears in shell history, process listings (ps aux), and potentially in Dagger's execution logs. Fix: Always use Dagger's secret type: --token=env:GITHUB_TOKEN. This passes secrets as opaque *dagger.Secret objects that are never serialized to strings in logs or state. In module code, accept *dagger.Secret parameters, not string.

Debug clue: If you see secrets in ps aux output or shell history, you are passing them as plain strings. Run history | grep -i token and ps aux | grep dagger to check for exposure. Switch to env: or file: secret sources immediately.


3. Version Mismatch Between CLI and Engine

You upgrade the Dagger CLI but the engine container from a previous version is still running. Calls fail with cryptic gRPC errors or version incompatibility messages. The engine auto-starts only if none is running. Fix: After upgrading the CLI, stop the old engine: docker rm -f $(docker ps -q -f name=dagger-engine). The next dagger call starts a fresh engine matching the CLI version. Pin the Dagger version in CI using the explicit version in the install script URL.


4. Assuming Pipeline Functions Are Executed Eagerly

You write a Dagger pipeline where each step calls the previous result. You expect each step to run immediately in sequence. Actually, Dagger builds a DAG and executes lazily — only resolving when you request a concrete value (like Stdout(ctx) or Sync(ctx)). Fix: Understand that Container methods return *dagger.Container (a lazy reference). Execution happens when you call terminal operations like .Stdout(ctx), .File(ctx), or .Sync(ctx). Design your pipeline around this — chain operations freely, then resolve at the end.

Under the hood: Dagger builds a directed acyclic graph (DAG) of operations, similar to how Terraform builds a resource graph. This enables caching, parallelism, and deduplication. The tradeoff is that errors surface at resolution time, not at the point where you define the operation — making stack traces less intuitive.


5. Using Absolute Host Paths Breaks Pipeline Portability

Your pipeline hardcodes WithMountedDirectory("/Users/alice/projects/myapp", ...) or references host-specific paths. The pipeline works on Alice's Mac but fails in CI where that path doesn't exist. Fix: Always pass source directories as function arguments: dagger call build --source=.. Inside the module, use relative paths within containers (/src, /app). Never reference host filesystem paths inside pipeline function definitions.


6. No Cache Volumes Defined — Every Run Is a Cold Start

Your pipeline installs npm packages, pip packages, or Go modules on every run because you never defined cache volumes. Each CI job takes 5+ minutes for dependency installation alone. Fix: Define cache volumes for package managers:

dag.CacheVolume("npm-cache")         // node_modules cache
dag.CacheVolume("go-mod-cache")      // Go module cache
dag.CacheVolume("pip-cache")         // Python pip cache
Mount them with WithMountedCache. Cache volumes persist across runs on the same runner.


7. dagger run vs dagger call Confusion

You use dagger run node ci.mjs expecting it to work like dagger call. dagger run executes an arbitrary script that uses the Dagger SDK directly (old pattern). dagger call invokes typed Dagger module functions. Mixing the two patterns causes confusion about what's a module function vs a raw script. Fix: For new projects, use Dagger modules (dagger init, dagger call). dagger run is for legacy scripts using the SDK directly, not for invoking module functions. Prefer the module pattern for its discoverability, type safety, and composability.


8. Forgetting That Containers Are Immutable — Mutations Don't Persist

You call .WithExec(["apt-get", "install", "curl"]) and then call .WithExec(["curl", "..."]) on the original container reference (not the returned one). The second exec runs in the original container without curl installed. Fix: Dagger containers are immutable value types. Every With* method returns a new container. Always chain: c = c.WithExec(...).WithExec(...). Never mutate a container reference in-place — always assign the return value.

Remember: This is the same pattern as Go's strings.Builder or Rust's builder pattern — each method returns a new value. The most common bug is container.WithExec(["apt-get", "install", "curl"]) without assigning the result: the install happened on a thrown-away copy.