TL;DR — Resumo Rápido

Dagger permite escrever pipelines CI/CD como código em Go, Python ou TypeScript que executam identicamente no seu laptop e em qualquer plataforma CI.

O Dagger é um motor de CI/CD programável que executa a lógica do pipeline dentro de contêineres, usando linguagens de programação reais em vez de YAML. Se você já passou horas depurando uma falha no GitHub Actions que nunca se reproduzia localmente, ou manteve cinco configurações de pipeline ligeiramente diferentes no GitHub, GitLab e Jenkins, o Dagger resolve ambos os problemas de uma vez: seu pipeline completo é código tipado que executa de forma idêntica no seu laptop e em qualquer plataforma CI. Este guia cobre a arquitetura, os padrões do SDK, serviços, cache, segredos e integração CI.

Pré-Requisitos

  • Docker Desktop ou Docker Engine em execução localmente
  • Node.js 18+ (para o SDK TypeScript), Go 1.21+ ou Python 3.11+ conforme o SDK escolhido
  • Familiaridade básica com contêineres e pelo menos um de Go, Python ou TypeScript
  • Um projeto para construir e testar (um app Node.js é usado nos exemplos)

Por Que Dagger: O Problema de Lock-In de Fornecedor

Cada plataforma CI principal tem seu próprio DSL para pipelines. O GitHub Actions usa YAML com steps uses:, o GitLab CI usa estágios em .gitlab-ci.yml, o Jenkins usa um DSL Declarativo baseado em Groovy e o CircleCI usa mais um schema YAML diferente. O resultado é que seu conhecimento e código de pipeline são completamente não portáteis.

O segundo problema é a lacuna “funciona na minha máquina”. Um sistema CI é um ambiente diferente da sua máquina de desenvolvimento. Depurar um pipeline com falha significa fazer push de um commit, aguardar um runner, ler logs truncados e repetir. Você não consegue executar o GitHub Actions localmente de nenhuma forma significativa sem soluções complexas.

O Dagger resolve isso com três decisões de design:

  1. Os pipelines são nativos de contêineres. Cada etapa executa em um contêiner via BuildKit. Cache, isolamento e portabilidade vêm do modelo de contêineres, não das abstrações da plataforma CI.
  2. Os pipelines são código real. Você escreve Go, Python ou TypeScript. Obtém tipos, testes, suporte de IDE e reutilização de código — coisas que YAML não pode oferecer.
  3. A integração CI é um wrapper fino. Seu YAML de CI se torna uma única invocação dagger call. Toda a lógica fica no seu código.
CaracterísticaDaggerGitHub ActionsGitLab CIJenkinsEarthly
Executa localmente sem modificaçãoSimNãoNãoParcialSim
Linguagem para lógica de pipelineGo/Python/TSYAMLYAMLGroovyDSL Earthfile
Lock-in de fornecedorNenhumAltoAltoMédioBaixo
Cache nativo de contêineresCamadas BuildKitLimitadoLimitadoNenhumCamadas BuildKit
Ecossistema de módulos reutilizáveisDaggerverseActions MarketplaceNenhumPluginsNenhum
Segurança de tiposCompletaNãoNãoParcialNão
Suporte de IDECompletoExtensõesExtensõesExtensõesNenhum

Arquitetura: Como o Dagger Funciona

A arquitetura do Dagger tem três camadas.

O Motor Dagger é um daemon de longa duração que encapsula o BuildKit. Quando você executa dagger call, o CLI se conecta ao Motor, que executa as etapas do pipeline em contêineres isolados. O Motor expõe uma API GraphQL sobre um socket Unix. Todos os clientes SDK se comunicam com ele via essa API.

Os clientes SDK (Go, Python, TypeScript, Elixir) geram wrappers fortemente tipados em torno da API GraphQL. Quando seu código TypeScript chama dag.container().withExec(["npm", "test"]), ele constrói uma consulta GraphQL enviada ao Motor. O Motor a avalia de forma lazy — nada executa até que você chame .stdout() ou .sync() para recuperar um resultado, permitindo ao Dagger otimizar o grafo de execução.

Os Módulos Dagger são a unidade de empacotamento. Um módulo é um diretório com um manifesto dagger.json, o código do seu pipeline e dependências. Os módulos podem ser publicados no Daggerverse (dagger.io/hub) e consumidos por outros módulos com dagger install.

Instalação

Instale o CLI dagger usando o script oficial:

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

No macOS com Homebrew:

brew install dagger/tap/dagger

O Docker deve estar em execução. O Dagger usa o Docker (ou qualquer daemon Moby compatível) como backend de contêineres. Verifique a conectividade:

docker info && dagger version

Escrevendo Pipelines em TypeScript

Inicialize um novo módulo Dagger no seu projeto:

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

Isso cria dagger.json e src/index.ts. Um pipeline completo de build e testes para um app Node.js:

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

@object()
export class MeuPipeline {
  @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();
  }
}

Execute localmente passando seu diretório atual como fonte:

dagger call test --source=.

A chamada withMountedCache cria um volume de cache persistente para node_modules que sobrevive entre execuções do pipeline.

Serviços: Bancos de Dados e Redis em Testes

A abstração Service do Dagger permite anexar contêineres efêmeros como serviços acessíveis por rede durante uma execução do pipeline. Isso elimina a necessidade de bancos de dados simulados em testes de integração.

@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();
}

Gerenciamento de Segredos

O Dagger tem um tipo Secret de primeira classe que garante que valores sensíveis nunca sejam escritos em logs, armazenados em cache ou expostos em rastreamentos.

Passe um segredo pelo CLI:

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

Use-o no 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();
}

Integração CI

A estratégia de integração CI do Dagger é sempre a mesma: manter o YAML de CI o mais fino possível. Toda a lógica permanece no 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=.

Em todos os casos, o comando dagger call test --source=. é idêntico. Trocar de provedor CI requer apenas modificar o wrapper fino — não reescrever a lógica do pipeline.

Cenário Real: Pipeline Completo Node.js

Você tem uma API Node.js que precisa de linting, testes unitários com PostgreSQL, build de imagem Docker e push para o GitHub Container Registry a cada merge na 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\ntestes: ok\npublicação ignorada (branch: ${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/minha-org/minha-api:latest");

  return `publicado: ${ref}`;
}

Armadilhas e Casos Extremos

Docker-in-Docker no CI — O Dagger requer acesso a um daemon Docker. No GitHub Actions com runners ubuntu-latest, o Docker vem pré-instalado. No GitLab CI, use o serviço docker:dind e configure DOCKER_HOST=tcp://docker:2376.

Monorepos grandes — Passar um diretório de monorepo completo como input Directory copia todos os arquivos para o sistema de arquivos do Dagger. Use withDirectory com um filtro ou passe apenas o subdiretório que seu pipeline precisa para manter as transferências rápidas.

Invalidação de cache — Os caches de withMountedCache são identificados pelo nome do volume. Se quiser caches separados por branch, use um nome de volume dinâmico como node-modules-${branch}.

Suporte no Windows — O Dagger requer um backend de contêineres Linux. No Windows, o Docker Desktop com WSL2 é necessário. O CLI dagger roda nativamente no Windows, mas a execução do pipeline sempre ocorre em contêineres Linux.

Resumo

  • O Dagger resolve o lock-in de fornecedor CI ao mover a lógica do pipeline para código nativo de contêineres (Go, Python, TypeScript)
  • O Motor Dagger encapsula o BuildKit e expõe uma API GraphQL; os clientes SDK fornecem wrappers tipados na linguagem de sua escolha
  • withMountedCache oferece cache persistente entre execuções para dependências sem plugins de cache CI personalizados
  • Os serviços (asService + withServiceBinding) fornecem bancos de dados e Redis reais para testes de integração sem mocks
  • Os segredos usam um parâmetro tipado Secret que nunca é registrado em logs ou armazenado em cache
  • A integração CI é sempre um wrapper fino chamando dagger call — comando idêntico no GitHub Actions, GitLab CI, CircleCI e Jenkins
  • Os Módulos Dagger são publicáveis no Daggerverse e combináveis — reutilize componentes de pipeline da comunidade
  • O Dagger Cloud adiciona visualização de rastreamentos, cache distribuído compartilhado persistente e monitoramento TUI em tempo real

Artigos Relacionados