Dagger: Portable CI/CD Pipelines as Code

Khimananda Oli 10 min read Database
Dagger: Portable CI/CD Pipelines as Code

By Khimananda Oli | Last reviewed: August 2026

Maintaining separate pipeline configurations for GitHub Actions, GitLab CI, and Jenkins creates unsustainable toil for engineering teams. Dagger: Portable CI/CD Pipelines as Code solves this fragmentation by letting you define build, test, and deployment logic in a general-purpose programming language like Go, Python, or TypeScript. Instead of learning yet another YAML dialect, you write standard functions that execute inside reproducible containers, ensuring your automation behaves identically whether triggered locally or in a managed cloud runner.

What makes Dagger: Portable CI/CD Pipelines as Code different from YAML?

Traditional CI systems treat pipeline definitions as static configuration data. This forces you to rely on string interpolation, fragile shell scripts, and vendor-specific action marketplaces to achieve basic logic. When you need conditional branching, loops, or error handling, YAML quickly becomes unmaintainable spaghetti. More critically, these pipelines are untestable outside the specific CI environment; you cannot run a GitHub Actions workflow on your laptop without significant mocking or third-party tools like comparing CI tools reveals the portability gap.

Dagger inverts this model. Your pipeline is an application, not a config file. Because it is written in a real programming language, you get type safety, IDE autocompletion, unit testing, and modular reuse out of the box. The execution engine abstracts away the underlying infrastructure, packaging every step into OCI-compliant containers. This means the "works on my machine" problem disappears entirely from your DevOps workflow.

Developer LaptopGo / Python / TSSDK Function CallDagger EngineAPI ServerContainer BuilderCache ManagerSecret StoreBuild ContainerAlpine + Go 1.23Test ContainerPostgres + AppDeploy ContainerAWS CLI + Helm
Dagger architecture: SDK calls trigger the engine to orchestrate isolated containers for each pipeline stage

The diagram above illustrates the core abstraction. Your code talks to the Dagger API, which then provisions ephemeral containers for each operation. There is no shared mutable state between steps unless you explicitly pass it. This isolation is what enables true portability; the container runtime is the only dependency, and that runtime exists everywhere.

How do you configure Dagger: Portable CI/CD Pipelines as Code for a Go project?

Setting up Dagger requires initializing a module in your repository root. This creates a dagger.json manifest and a scaffolded source directory where your pipeline logic lives. For a typical Go backend service, the initialization command establishes the project structure and dependencies automatically.

dagger init --sdk=go --source=./ci --name=myapp-pipeline

After initialization, you define your pipeline as exported methods on a struct. Each method represents a callable function that can be invoked via the CLI or composed into larger workflows. Below is a practical example of a build-and-test pipeline that mounts source code, runs tests against a service dependency, and produces a binary artifact.

package main

import (
    "context"
    "dagger/myapp-pipeline/internal/dagger"
)

type MyappPipeline struct{}

func (m *MyappPipeline) Test(ctx context.Context, source *dagger.Directory) (string, error) {
    postgres := dag.Container().
        From("postgres:16-alpine").
        WithEnvVariable("POSTGRES_PASSWORD", "testpass").
        WithExposedPort(5432).
        AsService()

    return dag.Container().
        From("golang:1.23-alpine").
        WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod")).
        WithMountedCache("/root/.cache/go-build", dag.CacheVolume("go-build")).
        WithServiceBinding("db", postgres).
        WithEnvVariable("DATABASE_URL", "postgres://postgres:testpass@db:5432/test?sslmode=disable").
        WithDirectory("/app", source).
        WithWorkdir("/app").
        WithExec([]string{"go", "test", "-v", "./..."}).
        Stdout(ctx)
}

Several details here matter for production use. The WithMountedCache directives persist Go module downloads and build artifacts across runs, reducing subsequent execution time from minutes to seconds. The database service starts automatically when bound and tears down when the pipeline completes. Notice there are no cleanup scripts or docker-compose files to manage; the dependency lifecycle is declarative and scoped to this single function call.

Running the pipeline locally and in CI

You invoke this function directly from your terminal during development:

dagger call test --source=. 

In your CI vendor's YAML, the configuration shrinks to a single command. Whether you use GitHub Actions, GitLab CI, or Jenkins, the runner simply installs the Dagger CLI and executes the same dagger call command. The pipeline logic never changes. If you need to add a linting step or change the database version, you modify the Go code once and every environment picks up the change immediately. This is the practical reality of Dagger: Portable CI/CD Pipelines as Code; the YAML becomes a thin invocation layer rather than a logic definition.

How does Dagger compare to GitHub Actions and traditional CI tools?

Choosing between Dagger and native CI tooling depends on your team's scale, compliance requirements, and tolerance for vendor lock-in. Having migrated teams across multiple platforms, I evaluate these tools against concrete operational criteria rather than feature checklists. The table below reflects real-world trade-offs observed in production environments throughout 2026.

CriteriaDaggerGitHub Actions / GitLab CIJenkins
Local reproducibilityIdentical execution via CLIRequires act/nektos or remote debuggingNearly impossible without full cluster
Language & type safetyGo, Python, TypeScript with typesYAML + shell scriptingGroovy/Jenkinsfile (weakly typed)
Vendor portabilityRuns on any OCI runtimeLocked to vendor ecosystemSelf-hosted but config not portable
Caching granularityPer-operation content-addressableArtifact upload/download per jobWorkspace-based, manual management
Secret handlingEphemeral, never touches diskEnvironment variables, masked logsCredentials store, plugin-dependent
Learning curveRequires programming proficiencyLow barrier, YAML familiarHigh, Groovy + plugin ecosystem
Best fitMulti-cloud, complex builds, complianceSingle-vendor, simple workflowsLegacy enterprise, custom plugins

The critical distinction is testability. With Dagger, you can write unit tests for your pipeline functions themselves. You can mock directory inputs, assert on container configurations, and verify secret injection patterns before ever running a build. Traditional CI tools offer no such capability; you discover bugs only after pushing to a branch and waiting for feedback. For teams practicing shift-left security, this testability is non-negotiable.

Traditional YAML Approach (Vendor Lock-In)GitHub Actions YAMLGitLab CI YAMLJenkinsfileAzure Pipelines YAML(Rewrite required)Each vendor requires separate config • No local execution • Untestable logicMigration = complete rewriteDagger Approach (Portable Pipeline as Code)Single Go/Python/TS ModuleVersioned • Tested • ReusableLocal LaptopGitHub ActionsGitLab CIAWS CodeBuild
Vendor lock-in comparison: YAML requires rewrites per platform while Dagger provides single-source portability

How do you handle secrets and compliance in Dagger pipelines?

Security and audit readiness are where many CI tools fail silently. Environment variables leak into logs, secrets persist in container layers, and proving least-privilege access requires manual evidence collection. Dagger addresses these concerns architecturally through its secret primitive and ephemeral execution model.

Secrets in Dagger are never exposed as environment variables unless explicitly converted, and even then they are masked in all output streams. More importantly, secrets are passed as mounted files or arguments that exist only for the duration of a single container operation. They never appear in image layers, cache volumes, or build history. This matters enormously for SOC 2 compliance automation because you can demonstrate cryptographically that secrets were handled correctly without relying on trust.

func (m *MyappPipeline) Deploy(ctx context.Context, source *dagger.Directory, awsAccessKey *dagger.Secret, awsSecretKey *dagger.Secret) error {
    _, err := dag.Container().
        From("amazon/aws-cli:latest").
        WithSecretVariable("AWS_ACCESS_KEY_ID", awsAccessKey).
        WithSecretVariable("AWS_SECRET_ACCESS_KEY", awsSecretKey).
        WithDirectory("/app", source).
        WithWorkdir("/app").
        WithExec([]string{"aws", "s3", "sync", "./dist", "s3://my-app-bucket"}).
        Sync(ctx)
    return err
}

For teams operating under ISO 27001 or SOC frameworks, this pattern provides auditable proof. The pipeline code itself documents exactly which secrets are used, where they are injected, and what operations they authorize. Combined with Dagger's built-in tracing and OpenTelemetry export, you gain end-to-end observability into pipeline execution that satisfies auditor requirements without additional tooling. When integrating with Kubernetes secrets management, Dagger can pull credentials directly from Vault or AWS Secrets Manager at runtime, avoiding static credential storage entirely.

Practical considerations for Nepal-based and distributed teams

For teams in Nepal working with international clients or managing multi-region deployments, Dagger's portability offers specific advantages. You can develop and test pipelines locally on modest hardware, then deploy to AWS Mumbai or Singapore regions without modification. The containerized execution model also sidesteps inconsistencies between developer machines running different OS versions—a common pain point when team members use Ubuntu, macOS, and Windows interchangeably. Since Dagger modules are version-controlled alongside application code, onboarding new engineers becomes a matter of cloning a repo and running dagger call, eliminating days of environment setup documentation.

When should you adopt Dagger versus sticking with native CI?

Dagger is not universally superior. Teams with simple, single-vendor workflows may find the initial learning overhead unjustified. The decision matrix I use with clients considers four factors: pipeline complexity, multi-environment requirements, compliance obligations, and team programming proficiency.

  • Adopt Dagger when: You maintain pipelines across two or more CI vendors, require local reproducibility for complex builds, need to enforce security policies programmatically, or have compliance audits demanding evidence of pipeline integrity.
  • Stay with native CI when: Your workflows are straightforward linear sequences, you operate exclusively within one vendor ecosystem, your team lacks Go/Python/TypeScript fluency, or your build times are already acceptable with existing caching.
  • Hybrid approach: Use Dagger for core build/test/deploy logic while retaining native YAML for vendor-specific integrations like PR comments, approval gates, or marketplace actions. This gives you portability where it matters without abandoning ecosystem conveniences.

The migration path is incremental. You do not need to rewrite everything at once. Start by extracting your most painful, flaky, or duplicated pipeline segment into a Dagger module. Validate it locally. Wire it into your existing CI as a single step. Expand from there. This measured approach reduces risk and lets the team build muscle memory gradually.

Start EvaluationMultiple CI vendors?YES → Strong candidateNO → Check nextComplex builds/tests?YES → Adopt DaggerNO → Stay native CIYesNoYesNo
Decision framework: evaluate multi-vendor needs and build complexity before adopting Dagger

Implementing Dagger: Portable CI/CD Pipelines as Code in Production

Adopting Dagger: Portable CI/CD Pipelines as Code fundamentally changes how your team reasons about automation. You gain reproducibility, testability, and vendor independence at the cost of requiring programming discipline in your DevOps practice. For organizations building toward SOC 2, ISO 27001, or multi-cloud resilience, this trade-off pays dividends quickly. The ability to audit pipeline logic as code, verify secret handling programmatically, and demonstrate identical behavior across environments transforms compliance from a quarterly panic into continuous confidence.

Start small. Extract one painful workflow. Prove the value locally. Then expand systematically. If your team needs guidance on migration strategy, compliance-ready pipeline design, or evaluating whether Dagger fits your specific architecture, reach out to discuss your infrastructure. Helping teams build automation that survives audits and scales safely is exactly the work I focus on.

Frequently Asked Questions

Dagger is an open-source engine that runs CI/CD pipelines as code inside containers, ensuring identical execution locally and in any cloud provider without vendor lock-in.

GitHub Actions uses YAML workflows tied to their platform, while Dagger defines pipelines programmatically using SDKs that run identically on any CI system or local machine.

Yes, the core Dagger engine is Apache 2.0 licensed and free for commercial use, though Dagger Cloud offers paid observability and collaboration features for teams.

Dagger provides official SDKs for Go, Python, Node.js, PHP, Java, and Rust, allowing developers to write pipeline logic in their preferred language instead of YAML.

No, Dagger replaces pipeline definitions, not orchestration. It integrates with Jenkins, GitLab CI, and GitHub Actions as the execution runtime for portable pipeline logic.

Install via the official script curl -L https://dl.dagger.io/dagger/install.sh | sh which places the binary in your PATH and starts the required containerized engine daemon automatically.

Yes, pass registry credentials as secrets using the WithRegistryAuth method in your SDK code, keeping tokens encrypted and never exposed in logs or environment variables.

Dagger caches container layers and operation results based on content hashes, automatically reusing previous outputs when inputs remain unchanged across local and remote executions.

Dagger Cloud provides centralized logging, real-time trace visualization, and team collaboration features that complement the open-source engine without being required for pipeline execution.

Secrets are injected at runtime through the engine API and never written to disk, environment variables, or layer history, preventing accidental exposure in cached images.

Yes, run dagger call with the --interactive flag to drop into a shell inside the exact container state where the failure occurred for live troubleshooting.

Yes, Dagger runs as standard containers compatible with Tekton, Argo Workflows, and Flux, making it portable across Kubernetes-based CI systems without modification.

Wrap shell commands using the Container.WithExec SDK method, then gradually replace imperative scripts with typed API calls for better caching, testing, and portability.

Uncached operations, excessive container startups, and missing volume mounts cause slowdowns; profile with dagger --debug and structure pipelines to maximize layer reuse and parallel execution.

Run dagger call locally to validate logic, use unit tests against the SDK, and leverage the interactive debugger to verify behavior matches production CI environments exactly.