TL;DR — Resumen Rápido

Dagger te permite escribir pipelines CI/CD como código en Go, Python o TypeScript que se ejecutan igual en tu laptop y en cualquier plataforma CI.

Dagger es un motor de CI/CD programable que ejecuta la lógica de tu pipeline dentro de contenedores, usando lenguajes de programación reales en lugar de YAML. Si alguna vez pasaste horas depurando un fallo en GitHub Actions que nunca se reproducía localmente, o manteniste cinco configuraciones de pipeline ligeramente distintas en GitHub, GitLab y Jenkins, Dagger resuelve ambos problemas a la vez: tu pipeline completo es código tipado que se ejecuta de forma idéntica en tu laptop y en cualquier plataforma CI. Esta guía cubre la arquitectura, los patrones del SDK, servicios, caché, secretos e integración CI.

Requisitos Previos

  • Docker Desktop o Docker Engine en ejecución localmente
  • Node.js 18+ (para el SDK de TypeScript), Go 1.21+ o Python 3.11+ según el SDK elegido
  • Familiaridad básica con contenedores y al menos uno de Go, Python o TypeScript
  • Un proyecto que quieras construir y probar (se usa una app Node.js en los ejemplos)

Por Qué Dagger: El Problema del Bloqueo de Proveedor

Cada plataforma CI importante tiene su propio DSL para pipelines. GitHub Actions usa YAML con pasos uses:, GitLab CI usa etapas en .gitlab-ci.yml, Jenkins usa un DSL Declarativo basado en Groovy y CircleCI usa otro esquema YAML distinto. El resultado es que tu conocimiento y código de pipeline son completamente no portables.

El segundo problema es la brecha “funciona en mi máquina”. Un sistema CI es un entorno diferente al de tu máquina de desarrollo. Depurar un pipeline fallido significa hacer push de un commit, esperar un runner, leer logs truncados y repetir. No puedes ejecutar GitHub Actions localmente de ninguna manera significativa sin soluciones complejas.

Dagger resuelve esto con tres decisiones de diseño:

  1. Los pipelines son nativos de contenedores. Cada paso se ejecuta en un contenedor via BuildKit. La caché, el aislamiento y la portabilidad provienen del modelo de contenedores, no de las abstracciones de la plataforma CI.
  2. Los pipelines son código real. Escribes Go, Python o TypeScript. Obtienes tipos, pruebas, soporte de IDE y reutilización de código — cosas que YAML no puede ofrecer.
  3. La integración CI es un wrapper delgado. Tu YAML de CI se convierte en una única invocación dagger call. Toda la lógica permanece en tu código.
CaracterísticaDaggerGitHub ActionsGitLab CIJenkinsEarthly
Ejecuta localmente sin modificaciónNoNoParcial
Lenguaje para lógica de pipelineGo/Python/TSYAMLYAMLGroovyDSL Earthfile
Bloqueo de proveedorNingunoAltoAltoMedioBajo
Caché nativa de contenedoresCapas BuildKitLimitadaLimitadaNingunaCapas BuildKit
Ecosistema de módulos reutilizablesDaggerverseActions MarketplaceNingunoPluginsNinguno
Seguridad de tiposCompletaNoNoParcialNo
Soporte de IDECompletoExtensionesExtensionesExtensionesNinguno

Arquitectura: Cómo Funciona Dagger

La arquitectura de Dagger tiene tres capas.

El Motor Dagger es un daemon de larga duración que envuelve BuildKit. Cuando ejecutas dagger call, el CLI se conecta al Motor, que ejecuta los pasos de tu pipeline en contenedores aislados. El Motor expone una API GraphQL sobre un socket Unix. Todos los clientes SDK se comunican con él via esta API.

Los clientes SDK (Go, Python, TypeScript, Elixir) generan wrappers fuertemente tipados alrededor de la API GraphQL. Cuando tu código TypeScript llama dag.container().withExec(["npm", "test"]), construye una consulta GraphQL que se envía al Motor. El Motor la evalúa de forma lazy — nada se ejecuta hasta que llamas .stdout() o .sync() para recuperar un resultado, lo que permite a Dagger optimizar el grafo de ejecución.

Los Módulos Dagger son la unidad de empaquetado. Un módulo es un directorio con un manifiesto dagger.json, el código de tu pipeline y sus dependencias. Los módulos pueden publicarse en el Daggerverse (dagger.io/hub) y ser consumidos por otros módulos con dagger install.

Instalación

Instala el CLI de dagger usando el script oficial:

curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh
dagger version

En macOS con Homebrew:

brew install dagger/tap/dagger

Docker debe estar en ejecución. Dagger usa Docker (o cualquier daemon Moby compatible) como backend de contenedores. Verifica la conectividad:

docker info && dagger version

Escribir Pipelines en TypeScript

Inicializa un nuevo módulo Dagger en tu proyecto:

dagger init --sdk=typescript --name=mi-pipeline

Esto crea dagger.json y src/index.ts. Un pipeline completo de construcción y pruebas para una app Node.js:

import { dag, Container, Directory, object, func } from "@dagger.io/dagger";

@object()
export class MiPipeline {
  @func()
  async build(source: Directory): Promise<Container> {
    const nodeCache = dag.cacheVolume("node-modules");

    return dag
      .container()
      .from("node:20-alpine")
      .withMountedDirectory("/app", source)
      .withMountedCache("/app/node_modules", nodeCache)
      .withWorkdir("/app")
      .withExec(["npm", "ci"])
      .withExec(["npm", "run", "build"]);
  }

  @func()
  async test(source: Directory): Promise<string> {
    const built = await this.build(source);
    return built
      .withExec(["npm", "test", "--", "--forceExit"])
      .stdout();
  }
}

Ejecútalo localmente pasando tu directorio actual como fuente:

dagger call test --source=.

La llamada withMountedCache crea un volumen de caché persistente para node_modules que sobrevive entre ejecuciones del pipeline.

Escribir Pipelines en Go

El mismo pipeline en Go:

package main

import (
    "context"
    "dagger.io/dagger"
)

type MiPipeline struct{}

func (m *MiPipeline) Test(ctx context.Context, source *dagger.Directory) (string, error) {
    nodeCache := dag.CacheVolume("node-modules")

    return dag.Container().
        From("node:20-alpine").
        WithMountedDirectory("/app", source).
        WithMountedCache("/app/node_modules", nodeCache).
        WithWorkdir("/app").
        WithExec([]string{"npm", "ci"}).
        WithExec([]string{"npm", "test", "--", "--forceExit"}).
        Stdout(ctx)
}

Servicios: Bases de Datos y Redis en Pruebas

La abstracción Service de Dagger te permite adjuntar contenedores efímeros como servicios accesibles por red durante una ejecución del pipeline. Esto elimina la necesidad de bases de datos simuladas en pruebas de integración.

@func()
async integrationTest(source: Directory): Promise<string> {
  const postgres = dag
    .container()
    .from("postgres:16-alpine")
    .withEnvVariable("POSTGRES_PASSWORD", "test")
    .withEnvVariable("POSTGRES_DB", "testdb")
    .withExposedPort(5432)
    .asService();

  return dag
    .container()
    .from("node:20-alpine")
    .withMountedDirectory("/app", source)
    .withWorkdir("/app")
    .withServiceBinding("db", postgres)
    .withEnvVariable("DATABASE_URL", "postgresql://postgres:test@db:5432/testdb")
    .withExec(["npm", "ci"])
    .withExec(["npm", "run", "test:integration"])
    .stdout();
}

Gestión de Secretos

Dagger tiene un tipo Secret de primera clase que garantiza que los valores sensibles nunca se escriban en logs, se almacenen en caché ni se expongan en trazas.

Pasa un secreto desde el CLI:

dagger call publish --source=. --registry-token=env:REGISTRY_TOKEN

Úsalo en tu pipeline TypeScript:

@func()
async publish(source: Directory, registryToken: Secret): Promise<string> {
  return dag
    .container()
    .from("node:20-alpine")
    .withMountedDirectory("/app", source)
    .withWorkdir("/app")
    .withExec(["npm", "ci"])
    .withExec(["npm", "run", "build"])
    .withSecretVariable("NPM_TOKEN", registryToken)
    .withExec(["npm", "publish"])
    .stdout();
}

Integración CI

La estrategia de integración CI de Dagger es siempre la misma: mantener el YAML de CI tan delgado como sea posible. Toda la lógica permanece en el módulo.

GitHub Actions:

name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dagger/dagger-for-github@v6
        with:
          verb: call
          args: test --source=.
          cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }}

GitLab CI:

test:
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh
  script:
    - dagger call test --source=.

En todos los casos, el comando dagger call test --source=. es idéntico. Cambiar de proveedor CI solo requiere modificar el wrapper delgado, no reescribir la lógica del pipeline.

Escenario Real: Pipeline Completo Node.js

Tienes una API Node.js que necesita linting, pruebas unitarias con una base de datos PostgreSQL, construcción de imagen Docker y push a GitHub Container Registry en cada merge a main.

@func()
async ci(
  source: Directory,
  registryToken: Secret,
  branch: string
): Promise<string> {
  const [lintResult, testResult] = await Promise.all([
    this.lint(source),
    this.integrationTest(source),
  ]);

  if (branch !== "main") {
    return `lint: ok\npruebas: ok\npublicación omitida (rama: ${branch})`;
  }

  const ref = await dag
    .container()
    .from("node:20-alpine")
    .withMountedDirectory("/app", source)
    .withWorkdir("/app")
    .withExec(["npm", "ci"])
    .withExec(["npm", "run", "build"])
    .withRegistryAuth("ghcr.io", "github-actions", registryToken)
    .publish("ghcr.io/mi-org/mi-api:latest");

  return `publicado: ${ref}`;
}

Problemas Comunes y Casos Extremos

Docker-in-Docker en CI — Dagger requiere acceso a un daemon Docker. En GitHub Actions con runners ubuntu-latest, Docker viene preinstalado. En GitLab CI, usa el servicio docker:dind y configura DOCKER_HOST=tcp://docker:2376.

Monorepos grandes — Pasar un directorio de monorepo completo como input Directory copia todos los archivos al sistema de archivos de Dagger. Usa withDirectory con un filtro o pasa solo el subdirectorio que necesita tu pipeline para mantener las transferencias rápidas.

Invalidación de caché — Los cachés de withMountedCache se identifican por el nombre del volumen. Si quieres cachés separados por rama, usa un nombre de volumen dinámico como node-modules-${branch}.

Soporte en Windows — Dagger requiere un backend de contenedores Linux. En Windows, se necesita Docker Desktop con WSL2. El CLI de dagger funciona nativamente en Windows, pero la ejecución del pipeline siempre ocurre en contenedores Linux.

Resumen

  • Dagger resuelve el bloqueo de proveedor CI al mover la lógica del pipeline a código nativo de contenedores (Go, Python, TypeScript)
  • El Motor Dagger envuelve BuildKit y expone una API GraphQL; los clientes SDK proporcionan wrappers tipados en tu lenguaje preferido
  • withMountedCache ofrece caché persistente entre ejecuciones para dependencias sin plugins de caché CI personalizados
  • Los servicios (asService + withServiceBinding) proporcionan bases de datos y Redis reales para pruebas de integración sin mocks
  • Los secretos usan un parámetro tipado Secret que nunca se registra en logs ni se almacena en caché
  • La integración CI es siempre un wrapper delgado que llama dagger call — comando idéntico en GitHub Actions, GitLab CI, CircleCI y Jenkins
  • Los Módulos Dagger son publicables en el Daggerverse y componibles — reutiliza componentes de pipeline de la comunidad
  • Dagger Cloud agrega visualización de trazas, caché distribuida compartida persistente y monitoreo TUI en tiempo real

Artículos Relacionados