tutorial

How to Monitor Dagger CI/CD Pipelines with Vigilmon

Dagger is a portable CI/CD engine that runs your pipelines inside containers. You write pipelines in code — Go, Python, TypeScript, or PHP — using Dagger's S...

Dagger is a portable CI/CD engine that runs your pipelines inside containers. You write pipelines in code — Go, Python, TypeScript, or PHP — using Dagger's SDK, and the Dagger engine executes them identically on your laptop, in GitHub Actions, on GitLab CI, or on any container runtime. The same pipeline code runs everywhere, and results are cached in a content-addressed layer store shared across runs.

This portability and caching power comes with observability challenges. Dagger pipelines run inside an engine process, modules resolve at execution time, and cache state accumulates silently. When something goes wrong — engine connectivity, module resolution failure, cache corruption — the failure can be opaque and delayed. This tutorial shows you how to monitor Dagger pipelines and the services they build with Vigilmon.


Why Dagger pipelines need external monitoring

Dagger abstracts pipeline execution behind the engine, which creates failure modes that differ from traditional CI:

  • Engine health degradation — the Dagger engine process consumes memory and file descriptors over time; a long-running engine in a persistent runner can OOM or exhaust inodes, causing new pipeline calls to hang or fail with cryptic errors
  • Module resolution failure — Dagger modules are resolved from Git refs or OCI images at execution time; a deleted tag, a broken module registry, or a private module with expired credentials causes dagger call to fail before any pipeline code runs
  • Cache poisoning or corruption — Dagger's content-addressed cache is robust but not immune to disk corruption or partial writes from interrupted runs; a poisoned cache entry causes builds to silently reuse a broken artifact
  • SDK integration breakage — upgrading the Dagger SDK in dagger.json without updating the engine version causes protocol version mismatches that appear as confusing gRPC errors
  • Pipeline execution timeouts masked by caching — a long step that used to be cached now runs live and hits the CI job timeout; the pipeline fails not because of a code bug but because of a cache miss on a slow operation

External monitoring from Vigilmon adds the final verification layer: after a Dagger pipeline builds and deploys your service, Vigilmon probes the live service from outside your infrastructure to confirm it's actually running. A pipeline can exit 0 while the service is broken; Vigilmon catches that.


What you'll need

  • A project using Dagger for CI/CD (any SDK: Go, Python, TypeScript, PHP)
  • A service deployed as part of your Dagger pipeline
  • A container registry (any OCI-compatible registry)
  • A deployment target (Kubernetes, Docker host, etc.)
  • A free Vigilmon account

Step 1: Add a health check to your Dagger pipeline

Write a Dagger function that verifies your service's health endpoint is reachable immediately after deploy. Here's an example in Go:

// dagger/main.go
package main

import (
	"context"
	"dagger/pipeline/internal/dagger"
	"fmt"
)

type Pipeline struct{}

// Build builds the container image
func (p *Pipeline) Build(ctx context.Context, source *dagger.Directory) *dagger.Container {
	return dag.Container().
		From("golang:1.22-alpine").
		WithDirectory("/src", source).
		WithWorkdir("/src").
		WithExec([]string{"go", "build", "-o", "/app/server", "./cmd/server"}).
		From("alpine:3.19").
		WithFile("/app/server", dag.Container().
			From("golang:1.22-alpine").
			WithDirectory("/src", source).
			WithWorkdir("/src").
			WithExec([]string{"go", "build", "-o", "/app/server", "./cmd/server"}).
			File("/app/server"),
		).
		WithExposedPort(8080).
		WithEntrypoint([]string{"/app/server"})
}

// Publish builds and pushes the image to a registry
func (p *Pipeline) Publish(
	ctx context.Context,
	source *dagger.Directory,
	registry string,
	imageName string,
	tag string,
) (string, error) {
	container := p.Build(ctx, source)
	ref := fmt.Sprintf("%s/%s:%s", registry, imageName, tag)
	digest, err := container.Publish(ctx, ref)
	if err != nil {
		return "", err
	}
	return digest, nil
}

// HealthCheck verifies a URL returns HTTP 200
func (p *Pipeline) HealthCheck(ctx context.Context, url string) (string, error) {
	result, err := dag.Container().
		From("curlimages/curl:latest").
		WithExec([]string{
			"curl", "-sf",
			"--max-time", "15",
			"--retry", "3",
			"--retry-delay", "5",
			"-w", "HTTP %{http_code} from %{url_effective}",
			"-o", "/dev/null",
			url,
		}).
		Stdout(ctx)
	if err != nil {
		return "", fmt.Errorf("health check failed for %s: %w", url, err)
	}
	return result, nil
}

// Deploy runs the full CI pipeline: build → publish → deploy → health check
func (p *Pipeline) Deploy(
	ctx context.Context,
	source *dagger.Directory,
	registry string,
	imageName string,
	tag string,
	serviceURL string,
) (string, error) {
	// Build and push
	digest, err := p.Publish(ctx, source, registry, imageName, tag)
	if err != nil {
		return "", fmt.Errorf("publish failed: %w", err)
	}

	// Health check the deployed service (assumes deploy is handled externally or by a run action)
	health, err := p.HealthCheck(ctx, serviceURL+"/healthz")
	if err != nil {
		return "", fmt.Errorf("health check after deploy failed: %w", err)
	}

	return fmt.Sprintf("Published: %s\nHealth: %s", digest, health), nil
}

Run the pipeline locally or in CI:

# Run the full deploy pipeline
dagger call deploy \
  --source=. \
  --registry=gcr.io/your-project \
  --image-name=api-service \
  --tag=${GIT_SHA} \
  --service-url=https://api.yourdomain.com

Step 2: Monitor the Dagger engine health

The Dagger engine runs as a container itself. In persistent CI environments (self-hosted runners, Kubernetes agents), monitor engine health explicitly:

#!/bin/bash
# check-dagger-engine.sh — run as a pre-pipeline step

set -euo pipefail

echo "=== Dagger engine check ==="

# Verify the engine responds
if ! dagger version --timeout 10s 2>/dev/null; then
  echo "ERROR: Dagger engine unresponsive"

  # Restart engine and log
  docker restart dagger-engine 2>/dev/null || true
  sleep 5

  if ! dagger version; then
    echo "FATAL: Dagger engine restart failed"
    # Ping Vigilmon with a failure event
    curl -s -X POST "https://api.vigilmon.online/v1/monitors/${VIGILMON_MONITOR_ID}/incidents" \
      -H "Authorization: Bearer ${VIGILMON_API_KEY}" \
      -H "Content-Type: application/json" \
      -d '{"title":"Dagger engine unresponsive on '"$(hostname)"'","severity":"critical"}'
    exit 1
  fi
fi

# Check engine resource usage
ENGINE_PID=$(pgrep -f "dagger-engine" | head -1)
if [ -n "$ENGINE_PID" ]; then
  MEM_MB=$(ps -o rss= -p "$ENGINE_PID" | awk '{print int($1/1024)}')
  if [ "$MEM_MB" -gt 8192 ]; then
    echo "WARNING: Dagger engine memory usage: ${MEM_MB}MB — consider restarting"
  fi
fi

echo "Dagger engine: OK"

Step 3: Track cache hit rates

Dagger's caching is a key performance feature. Monitor cache effectiveness by capturing pipeline timing and comparing with and without cache:

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

@object()
export class Pipeline {
  @func()
  async buildWithMetrics(source: Directory): Promise<string> {
    const start = Date.now();

    const built = await dag
      .container()
      .from("node:20-alpine")
      .withDirectory("/app", source)
      .withWorkdir("/app")
      .withExec(["npm", "ci"])
      .withExec(["npm", "run", "build"])
      .sync();

    const durationMs = Date.now() - start;

    // A build under 10s likely hit cache; over 60s suggests cache miss
    const cacheStatus = durationMs < 10000 ? "cache_hit" : "cache_miss";

    // Return metrics as JSON for the caller to parse and forward to monitoring
    return JSON.stringify({
      durationMs,
      cacheStatus,
      timestamp: new Date().toISOString(),
    });
  }
}
# In CI: capture metrics and forward to monitoring
METRICS=$(dagger call build-with-metrics --source=.)
DURATION=$(echo "$METRICS" | jq -r '.durationMs')
CACHE_STATUS=$(echo "$METRICS" | jq -r '.cacheStatus')

echo "Build: ${DURATION}ms (${CACHE_STATUS})"

# Alert if cache miss rate is climbing (track in your APM or a simple webhook)
if [ "$CACHE_STATUS" = "cache_miss" ] && [ "$DURATION" -gt 120000 ]; then
  echo "WARNING: Slow build with cache miss — check Dagger cache health"
fi

Step 4: Set up Vigilmon monitoring for deployed services

After each Dagger pipeline successfully builds and deploys your service, Vigilmon monitors it continuously:

  1. Log in to Vigilmon
  2. Click New Monitor → HTTP(S)
  3. URL: https://api.yourdomain.com/healthz
  4. Interval: 1 minute
  5. Response validation: status 200, body contains "status":"ok"
  6. Regions: US East, EU West, Asia Pacific
  7. Click Save

Add a deploy notification to your Dagger pipeline so Vigilmon correlates deployment events with uptime:

// In your Deploy function, after successful health check
func notifyVigilmon(ctx context.Context, monitorID, apiKey, version string) error {
	_, err := dag.Container().
		From("curlimages/curl:latest").
		WithExec([]string{
			"curl", "-s", "-X", "POST",
			fmt.Sprintf("https://api.vigilmon.online/v1/monitors/%s/events", monitorID),
			"-H", "Authorization: Bearer " + apiKey,
			"-H", "Content-Type: application/json",
			"-d", fmt.Sprintf(`{"type":"deploy","version":"%s"}`, version),
		}).
		Stdout(ctx)
	return err
}

Step 5: Monitor module resolution in CI

Dagger module dependencies are declared in dagger.json. Add a pre-flight check in CI that verifies module resolution before the pipeline runs:

#!/bin/bash
# preflight-check.sh

echo "=== Dagger module resolution check ==="

# Attempt to resolve all modules without running any functions
if ! dagger mod sync --timeout 30s; then
  echo "ERROR: Dagger module sync failed — check dagger.json and module registry connectivity"

  curl -s -X POST "https://api.vigilmon.online/v1/monitors/${VIGILMON_MONITOR_ID}/incidents" \
    -H "Authorization: Bearer ${VIGILMON_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{
      "title": "Dagger module resolution failure in CI",
      "severity": "high",
      "details": "dagger mod sync failed on '"$GITHUB_REPOSITORY"' ref '"$GITHUB_SHA"'"
    }'

  exit 1
fi

echo "Module resolution: OK"

Step 6: Configure alerts

In Vigilmon, configure alerts for your Dagger-deployed service:

  • Email — immediate alert when the HTTP check fails from any region
  • Slack — post to #ci-alerts with a link to the failed Dagger pipeline run
  • PagerDuty — escalate to on-call after 3 consecutive failures
  • Webhook — trigger a Dagger re-deploy pipeline on transient failures

Set the confirmation threshold to 2 consecutive failures (2 minutes) to suppress noise from Dagger's brief engine startup time when a cold runner spins up.


What you're now monitoring

| Failure | How Vigilmon catches it | |---|---| | Dagger engine OOM on self-hosted runner | Pre-flight fails; pipeline aborts; Vigilmon opens incident | | Module resolution fails due to deleted tag | dagger mod sync preflight fails; service not redeployed; Vigilmon detects version staleness | | Cache poisoning causes broken artifact | HealthCheck function inside pipeline fails; Vigilmon also fails external probe | | SDK/engine version mismatch | dagger call exits with gRPC error; pipeline notifies Vigilmon of failed deploy | | Deployment succeeds but service crashes | Dagger pipeline exits 0; Vigilmon HTTP probe catches 5xx or timeout |


Conclusion

Dagger brings consistency and portability to CI/CD pipelines — the same code runs everywhere, and caching makes rebuilds fast. But the engine, module resolution, cache state, and the services your pipelines deploy all need monitoring. Adding Vigilmon to your Dagger workflow takes minutes: write a HealthCheck function in your Dagger module, notify Vigilmon on each deploy, and set up an HTTP monitor for your service. From that point on, every Dagger pipeline run is externally verified, and any failure — engine, module, cache, or deployment — generates an alert before users notice.

Start monitoring your Dagger pipelines with a free Vigilmon account.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →