Real Code vs. YAML Spaghetti
The difference between Dagger and YAML-based CI becomes most obvious when you need to do something even slightly dynamic. Consider a monorepo with five microservices that all need to be built and pushed to a container registry.
In YAML (GitHub Actions matrix strategy): a matrix job lists the five service names and, for each one, checks out the repo and runs a shell step that builds and pushes a Docker image tagged with the commit SHA, interpolating the matrix variable into the image name and build path.
This looks manageable. But now add: "skip the build if the service directory hasn't changed since the last commit," "use a different base image for the ui service," and "run integration tests only for payment and auth." The YAML grows into an unmaintainable tangle of conditionals, expressions, and workarounds.
In Dagger (Python SDK): the same pipeline is a Python function that loops over the service list, picks a base image per service using an ordinary conditional expression, builds each container from that base with its source directory mounted in, adds an integration-test step only for the services in a needs_integration_tests set, and publishes each one — with the five builds run concurrently via standard asyncio.gather, not a special CI-only concurrency primitive.
This is readable — a new team member can understand it without knowing Dagger-specific syntax. It is type-safe — your IDE provides autocomplete and catches errors before you run anything. It is testable — you can write a pytest test that verifies needs_integration_tests contains the right services. And it is debuggable locally — run python ci/pipeline.py on your laptop and watch it execute.
A common interview question is: "How do you handle code reuse in your CI/CD pipelines?" The expected answer at a senior level goes beyond "we use YAML anchors" or "we use shared libraries in Jenkins." The strong answer describes treating pipeline logic as first-class software: versioned modules, unit-tested functions, and a language with real abstraction capabilities. Dagger is the concrete example that backs this up.
The Portability Guarantee
Because your pipeline is just a Python (or Go, or TypeScript) script that calls the Dagger SDK, running it locally is identical to running it in CI. There is no special local runner to configure. You just need Docker running and the Dagger CLI installed. This means:
- Faster debugging: Fix pipeline failures on your laptop in seconds instead of minutes.
- Onboarding: New engineers can run the full CI pipeline locally on day one.
- Confidence: If the pipeline passes locally, it will pass in CI. Not probably — definitively.