DEV Community

Alex Spinov
Alex Spinov

Posted on

Dagger Has a Free API — Heres How to Run CI/CD Pipelines Locally Before Pushing

Dagger lets you write CI/CD pipelines as code — TypeScript, Python, or Go — and run them identically on your laptop and in CI. No more 'works in CI but not locally'.

Why Dagger?

  • Run locally: Test your entire CI pipeline before pushing
  • Any language: TypeScript, Python, Go, PHP
  • Any CI: GitHub Actions, GitLab CI, CircleCI, Jenkins
  • Caching: Automatic layer caching across runs
  • Containers: Everything runs in containers
  • Composable: Share and reuse pipeline modules

Install

# macOS/Linux
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh

# Init project
dagger init --sdk=typescript
Enter fullscreen mode Exit fullscreen mode

Define a Pipeline (TypeScript)

// dagger/src/index.ts
import { dag, Container, Directory, object, func } from '@dagger.io/dagger';

@object()
export class MyProject {
  @func()
  async build(source: Directory): Promise<Container> {
    return dag
      .container()
      .from('node:20-alpine')
      .withDirectory('/app', source)
      .withWorkdir('/app')
      .withExec(['npm', 'install'])
      .withExec(['npm', 'run', 'build']);
  }

  @func()
  async test(source: Directory): Promise<string> {
    return dag
      .container()
      .from('node:20-alpine')
      .withDirectory('/app', source)
      .withWorkdir('/app')
      .withExec(['npm', 'install'])
      .withExec(['npm', 'test'])
      .stdout();
  }

  @func()
  async lint(source: Directory): Promise<string> {
    return dag
      .container()
      .from('node:20-alpine')
      .withDirectory('/app', source)
      .withWorkdir('/app')
      .withExec(['npm', 'install'])
      .withExec(['npx', 'eslint', 'src/'])
      .stdout();
  }
}
Enter fullscreen mode Exit fullscreen mode

Run Locally

# Run tests
dagger call test --source=.

# Run build
dagger call build --source=.

# Run lint
dagger call lint --source=.
Enter fullscreen mode Exit fullscreen mode

Exact same pipeline runs in CI and on your laptop.

Use in GitHub Actions

name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dagger/dagger-for-github@v6
        with:
          verb: call
          args: test --source=.
Enter fullscreen mode Exit fullscreen mode

Pipeline with Database (Integration Tests)

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

  return dag
    .container()
    .from('node:20-alpine')
    .withDirectory('/app', source)
    .withWorkdir('/app')
    .withServiceBinding('db', db)
    .withEnvVariable('DATABASE_URL', 'postgres://postgres:test@db:5432/postgres')
    .withExec(['npm', 'install'])
    .withExec(['npm', 'run', 'test:integration'])
    .stdout();
}
Enter fullscreen mode Exit fullscreen mode

Publish Container

@func()
async publish(source: Directory, tag: string): Promise<string> {
  const container = await this.build(source);
  return container
    .withEntrypoint(['node', 'dist/index.js'])
    .publish(`registry.example.com/my-app:${tag}`);
}
Enter fullscreen mode Exit fullscreen mode

Real-World Use Case

A team spent 2 hours per week debugging CI-only failures. After switching to Dagger, they run the exact pipeline locally. CI debug time dropped to near zero because if it works locally, it works in CI.


Need to automate data collection? Check out my Apify actors for ready-made scrapers, or email spinov001@gmail.com for custom solutions.

Top comments (0)