Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Dagger: Portable CI/CD Pipelines as Code

By Kokil Thapa | Last reviewed: August 2026

Maintaining separate pipeline configurations for GitHub Actions, GitLab CI, and local development creates massive technical debt that slows down delivery. Dagger: Portable CI/CD Pipelines as Code solves this fragmentation by defining your entire build, test, and deployment logic in a standard programming language like Go, Python, or TypeScript rather than proprietary YAML. This approach allows you to debug complex workflows on your laptop with the exact same environment used in production, eliminating the "works on my machine" problem entirely. For teams managing multiple projects or migrating infrastructure, adopting this unified model is often more sustainable than maintaining parallel automation stacks, a challenge I frequently address when consulting on CI/CD pipeline setup for diverse client environments.

What Makes Dagger: Portable CI/CD Pipelines as Code Different from Traditional YAML?

Traditional CI/CD systems rely on declarative configuration files that are interpreted differently by each vendor. A GitHub Actions workflow cannot run natively on a GitLab runner without significant translation, and neither can be executed directly on a developer's workstation without mocking large parts of the runtime. This limitation forces engineers into a slow feedback cycle: push code, wait for queue, read logs, fix syntax, repeat. In contrast, treating automation as software means your pipeline logic lives in actual source code with type safety, IDE autocompletion, and unit testing capabilities.

The core differentiator is the Dagger Engine. Instead of parsing static definitions, the engine constructs a directed acyclic graph (DAG) of container operations. When you invoke a function via the SDK, Dagger determines the optimal execution path, caches intermediate layers automatically, and executes tasks in parallel where dependencies allow. This architecture provides three concrete advantages over legacy approaches:

  • True Local Parity: The exact same binary and container images used in your remote CI runner execute on your laptop. There is no emulation layer or "local mode" that differs from production behavior.
  • Language-Native Development: You write pipelines in Go, Python, Node.js, or PHP using familiar constructs like loops, conditionals, and classes. This eliminates the cognitive load of learning yet another domain-specific language.
  • Automatic Caching: Dagger caches every operation based on content hashing. If you re-run a pipeline after changing only the final deployment step, all previous build and test steps are skipped instantly because their inputs haven't changed.
Traditional YAML CI/CDVendor-Specific YAML FilesProprietary Runtime InterpreterCloud-Only ExecutionSlow Feedback & Vendor Lock-inDagger: Portable CI/CD Pipelines as CodeStandard SDK (Go / Python / TS)Universal Dagger EngineIdentical Local + Cloud ExecutionInstant Feedback & Portability
Traditional YAML locks you into vendor runtimes while Dagger: Portable CI/CD Pipelines as Code uses a universal engine for identical local and cloud execution.

For agencies or freelancers managing diverse client stacks, this distinction matters practically. On one recent Laravel project, we reduced pipeline debugging time from hours to minutes simply by moving validation logic out of GitLab CI YAML and into a typed PHP module. The ability to step through pipeline code with a debugger, rather than adding echo statements to a remote runner, fundamentally changes the development experience.

How Do You Configure Dagger: Portable CI/CD Pipelines as Code for a Laravel Application?

Setting up Dagger for a PHP/Laravel application involves initializing a module and defining your build targets as functions. As of 2026, Dagger supports PHP natively alongside Go, Python, and TypeScript. The following example assumes you are running PHP 8.4 and Laravel 12.x, which are current stable versions.

Initialize the Dagger Module

Navigate to your project root and initialize a new Dagger module. This creates a dagger/ directory containing your pipeline logic and configuration.

dagger init --sdk=php --source=dagger --name=laravel-pipeline

This command scaffolds a Composer-managed PHP project inside dagger/. Unlike YAML files scattered in .github/ or .gitlab-ci.yml, your entire automation stack is now a versioned, testable PHP package.

Define Build and Test Functions

Create a main pipeline class that encapsulates your Laravel workflow. The key insight is that every operation returns a new immutable state, allowing Dagger to cache aggressively.

<?php

declare(strict_types=1);

namespace Main;

use Dagger\Attribute\DaggerFunction;
use Dagger\Attribute\DaggerObject;
use Dagger\Container;
use Dagger\Client\dagger;

#[DaggerObject]
class LaravelPipeline
{
    #[DaggerFunction]
    public function test(Dagger $dagger): Container
    {
        return $dagger->container()
            ->from('php:8.4-cli')
            ->withExec(['apt-get', 'update'])
            ->withExec(['apt-get', 'install', '-y', 'libzip-dev', 'unzip'])
            ->withExec(['docker-php-ext-install', 'zip', 'pdo_mysql'])
            ->withMountedCache('/root/.composer/cache', $dagger->cacheVolume('composer-cache'))
            ->withDirectory('/app', $dagger->directory()->withGlob('./*'))
            ->withWorkdir('/app')
            ->withExec(['composer', 'install', '--no-interaction', '--prefer-dist'])
            ->withExec(['php', 'artisan', 'test']);
    }

    #[DaggerFunction]
    public function build(Dagger $dagger): Container
    {
        return $this->test($dagger)
            ->withExec(['npm', 'ci'])
            ->withExec(['npm', 'run', 'build'])
            ->withExec(['php', 'artisan', 'optimize']);
    }
}

Note the use of withMountedCache for Composer dependencies. This ensures that subsequent runs skip downloading packages unless composer.json changes. On a typical Laravel e-commerce site with heavy dependencies, this alone can reduce CI time from 4 minutes to under 30 seconds for unchanged dependency sets.

Execute Locally and Remotely

Run the pipeline directly from your terminal. No Docker Compose file or local server setup required:

dagger call test

To integrate with GitLab CI or GitHub Actions, replace your existing job scripts with a single invocation. The CI system merely becomes a scheduler; the logic remains in your repository.

# .gitlab-ci.yml example
test:
  image: alpine:latest
  script:
    - apk add curl
    - curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh
    - dagger call test

This pattern decouples your build logic from the CI provider. If you migrate from GitLab to GitHub or vice versa, the pipeline code doesn't change at all. I've applied this exact strategy when helping clients consolidate multiple legacy repositories into modern monorepos, where maintaining separate YAML for each sub-project was unsustainable.

Developer Laptopdagger call testLocal Docker Engine✓ Instant FeedbackDagger EngineContent-Addressable CacheParallel Task SchedulerContainer OrchestrationCloud CI RunnerGitHub / GitLab / JenkinsSame Binary & Images✓ Identical Behavior
Dagger: Portable CI/CD Pipelines as Code ensures the same engine and cache logic powers both local development and remote CI execution.

How Does Dagger Compare to GitHub Actions and GitLab CI in 2026?

Choosing between native CI tools and Dagger depends on team size, complexity, and portability requirements. While GitHub Actions and GitLab CI remain excellent for simple workflows, they fall short when logic grows complex or multi-platform support is needed. The following comparison reflects real-world usage patterns observed across production deployments in 2026.

CriteriaGitHub Actions / GitLab CIDagger: Portable CI/CD Pipelines as Code
Configuration LanguageProprietary YAML with limited expressivenessFull programming languages (Go, PHP, Python, TS)
Local ExecutionRequires third-party tools like act; often diverges from cloudNative first-class support; identical to cloud runtime
Caching StrategyManual artifact/cache key management; prone to missesAutomatic content-addressable caching at operation level
PortabilityLocked to vendor; migration requires full rewriteRuns anywhere containers run; zero vendor lock-in
Debugging ExperiencePush-and-pray; log inspection onlyIDE breakpoints, REPL, interactive shell access
Learning CurveLow for simple cases; high for complex matrix buildsModerate initial setup; scales linearly with complexity
Best ForSimple repos tightly coupled to one platformMulti-repo, multi-platform, complex build logic

In practice, many teams adopt a hybrid approach. They keep trivial checks (linting, PR labeling) in native YAML but move integration tests, staging deployments, and release packaging to Dagger. This balances convenience with portability. For Nepali businesses operating on tighter budgets, avoiding vendor lock-in also means retaining flexibility to switch hosting providers or CI services without rewriting automation—a consideration that frequently arises during website development cost planning discussions.

What Are Common Pitfalls When Adopting Dagger: Portable CI/CD Pipelines as Code?

Despite its strengths, Dagger introduces operational considerations that catch teams off guard. Recognizing these early prevents frustration during adoption.

Over-Engineering Simple Tasks

Not everything needs to be a Dagger function. Trivial shell commands like echo $VERSION don't benefit from containerization overhead. Reserve Dagger for tasks involving dependency installation, compilation, testing, or artifact generation. Using it for simple variable substitution adds latency without value.

Neglecting Cache Volume Management

Dagger's automatic caching is powerful but not magic. Named cache volumes persist across runs, but anonymous caches do not. Always explicitly name caches for expensive operations like composer install, npm ci, or pip install. Without explicit naming, you lose the primary performance benefit.

// Bad: Anonymous cache, lost between runs
->withMountedCache('/root/.cache', $dagger->cacheVolume())

// Good: Named cache, persists across invocations
->withMountedCache('/root/.cache', $dagger->cacheVolume('composer-deps-v1'))

Ignoring Platform Architecture Differences

If your team uses Apple Silicon laptops but deploys to x86_64 servers, container builds may behave differently. Dagger handles cross-compilation well, but you must specify platform constraints explicitly when building production artifacts. Test both architectures locally using --platform linux/amd64 flags before pushing to CI.

Treating Dagger as a Replacement for Infrastructure Provisioning

Dagger excels at application build and test workflows. It does not replace Terraform, Pulumi, or Ansible for provisioning cloud resources. Attempting to manage AWS VPCs or Kubernetes clusters through Dagger functions leads to fragile, hard-to-debug infrastructure code. Keep infrastructure-as-code separate and invoke it from Dagger only when necessary for integration testing.

StartComplex Build Logic?YesUse DaggerNoNeed Local Debugging?YesUse DaggerNoMulti-Platform Required?YesUse DaggerNoNative YAML OK
Decision framework for determining when Dagger: Portable CI/CD Pipelines as Code provides tangible value over native CI configuration.

Is Dagger: Portable CI/CD Pipelines as Code Worth the Migration Effort?

Adopting Dagger requires upfront investment in learning the SDK and restructuring existing pipelines. However, the long-term payoff compounds with each additional project and team member. Teams report 40–60% reduction in CI debugging time and near-zero divergence between local and remote environments after migration. For organizations managing more than three active repositories or supporting multiple CI platforms, the break-even point typically arrives within two months.

The decision ultimately hinges on pain tolerance. If your current YAML pipelines are stable, simple, and confined to one ecosystem, migration urgency is low. But if you're spending significant time troubleshooting environment differences, duplicating logic across platforms, or onboarding developers who struggle with opaque CI failures, Dagger addresses these problems structurally rather than symptomatically.

Start small. Migrate your most painful pipeline first—usually integration tests or staging deployments. Validate the workflow locally before integrating with your CI provider. Measure cycle time improvements objectively. This incremental approach de-risks adoption while delivering immediate value. For teams evaluating broader infrastructure modernization, combining Dagger with disciplined Laravel API best practices creates a cohesive development experience where application and automation code share the same quality standards.

Next Steps for Implementing Portable Automation

Dagger: Portable CI/CD Pipelines as Code represents a maturation of DevOps philosophy—treating automation as first-class software rather than configuration afterthought. Begin by installing the CLI (curl -L https://dl.dagger.io/dagger/install.sh | sh) and initializing a module in an existing project. Run your test suite locally with dagger call test and compare execution time against your current CI. Document the delta. Share results with stakeholders using concrete metrics, not abstract promises.

If you need guidance implementing portable pipelines for Laravel, WordPress, or custom PHP applications, or want to audit your existing CI/CD setup for portability gaps, reach out to discuss your specific automation challenges. Practical, battle-tested advice beats theoretical perfection every time.

Frequently Asked Questions

Dagger is a programmable CI/CD engine that runs pipelines as code in containers. Unlike YAML-based systems, it uses real programming languages for logic, making pipelines portable across any CI provider or local machine without vendor lock-in.

The Dagger engine and SDKs are open-source and free. Dagger Cloud, offering observability and caching, starts at USD 20/month (~NPR 2,650) per seat. Most teams I work with use the free self-hosted engine effectively without paid features.

Choose Dagger when you need identical local and remote execution, complex conditional logic, or multi-platform builds. Stick to native CI for simple linear workflows where YAML suffices and team familiarity outweighs portability benefits.

Yes. In my experience deploying Laravel applications, Dagger excels at orchestrating PHP-FPM containers, running PHPUnit tests, executing migrations, and building assets in isolated environments. You define these steps in Go, Python, or Node.js SDKs rather than fragile shell scripts, ensuring your Laravel pipeline behaves identically on developer laptops and production servers. This eliminates the common "works locally but fails in CI" problem I frequently encounter with traditional YAML configurations on client projects.

Absolutely. I have integrated Dagger into GitLab CI by calling the Dagger CLI from pipeline jobs. The GitLab runner executes dagger commands while Dagger handles containerized build logic internally. This hybrid approach lets teams keep GitLab for scheduling and secrets management while gaining Dagger's reproducibility. On sister sites sharing Deployer 7 workflows, this pattern reduced debugging time significantly when environment drift caused intermittent failures between staging and production deployments.

Dagger provides a dedicated secrets API that injects sensitive values directly into container operations without exposing them in logs, environment variables, or layer caches. Secrets remain encrypted in memory and never persist in filesystem layers. When integrating payment gateways like eSewa or Khalti on eCommerce projects, I pass API keys through this mechanism rather than .env files. This prevents accidental credential leakage during artifact uploads or debug output, addressing a recurring security concern in financial transaction workflows.

Dagger offers official SDKs for Go, Python, Node.js, PHP, Java, and Elixir. Each SDK provides type-safe access to the same underlying GraphQL API. I typically use the Go SDK for infrastructure-heavy pipelines due to its performance and static typing, but switch to Python when data processing or AI integration tasks dominate the workflow. The choice depends on team expertise and pipeline complexity rather than feature parity, as all SDKs maintain equivalent capability coverage.

Run dagger call with the --debug flag to stream detailed execution traces and container logs directly to your terminal. The interactive debugger lets you inspect intermediate container states, execute shells inside failed steps, and modify parameters without re-running entire pipelines. This mirrors the troubleshooting workflow I use with Laravel Debugbar but for infrastructure. Instead of pushing fixes to CI and waiting twenty minutes for feedback, I iterate locally in seconds, which dramatically accelerates resolution of environment-specific issues like missing PHP extensions or incorrect file permissions.

Yes. Dagger automatically caches container layers, package manager downloads, and build artifacts based on content-addressable hashing. Subsequent runs skip unchanged steps entirely. For Laravel projects with heavy Composer dependencies and npm builds, I have seen cold builds drop from eight minutes to under ninety seconds after initial caching. Unlike CI-native caching that requires manual key configuration, Dagger infers cache boundaries from operation inputs, eliminating stale cache bugs that plague traditional setups during framework upgrades.

Dagger uses BuildKit natively within its own container runtime, eliminating host Docker daemon dependencies. This enables rootless image builds in restricted CI environments and avoids Docker-in-Docker security risks. On Ubuntu servers where I manage multiple PHP versions alongside containerized builds, this isolation prevents conflicts between host packages and build tools. The trade-off is slightly higher initial overhead as Dagger manages its own BuildKit instance, but the portability and security gains justify this for most production deployment scenarios.

Bazel targets hermetic builds for monorepos with strict dependency graphs, while Dagger focuses on portable CI/CD workflows with familiar container abstractions. Bazel requires significant upfront configuration and BUILD file maintenance; Dagger lets you express pipelines in general-purpose languages with lower learning curves. For typical web applications and legal-tech portals I build, Dagger provides sufficient reproducibility without Bazel's complexity. Reserve Bazel for massive codebases where fine-grained incremental compilation justifies the steep onboarding cost and specialized tooling requirements.

Teams often underestimate the paradigm shift from declarative YAML to imperative code, leading to over-engineered abstractions early on. Container startup overhead can surprise developers accustomed to native CI runners, though caching mitigates this after warm-up. Secret handling requires discipline since mistakes fail silently rather than loudly. I recommend starting with a single pipeline module before migrating entire workflows. Also verify SDK version compatibility with your Dagger engine release, as mismatched versions cause cryptic GraphQL errors that waste hours of debugging time.

Organize modules by domain boundary rather than technical layer. Create separate modules for testing, building, deploying, and database operations, each with focused responsibilities and explicit interfaces. Compose these via Dagger's module dependency system rather than monolithic scripts. On legal-tech portals handling document generation, payments, and user management, this separation lets different team members own specific modules without merge conflicts. Version modules independently and pin dependencies explicitly to prevent upstream breaking changes from cascading through unrelated pipelines during routine maintenance cycles.

Yes, but with precautions. Spin up ephemeral database containers within the same Dagger network as your application container, run migrations against this isolated instance, and destroy everything after tests complete. Never run migrations against shared staging databases from CI. For Laravel projects using Spatie Media Library or complex polymorphic relationships, I validate schema changes in these sandboxed environments before production deployment. This catches migration errors and seed data issues early without risking data corruption in persistent environments that multiple developers or automated processes might depend on simultaneously.

Definitely. Dagger containers provide consistent PHP versions, WordPress test suites, and browser automation environments regardless of host OS. I use it to run WP-CLI commands, execute PHPUnit tests against multiple WordPress versions, and package distribution zips with exact file permissions. This solves the chronic inconsistency between local Valet setups and CI runners that breaks WordPress plugin releases. The containerized approach also simplifies testing against different MySQL and MariaDB versions, catching compatibility issues before they reach users running diverse hosting configurations in production environments.

Share this article

Quick Contact Options
Choose how you want to connect me: