Speed up CI/CD with Dagger Engine and Docker Registry Mirror
Table of Contents
This blog post covers how to deploy Dagger Engine with a Docker registry mirror on Kubernetes to speed up CI/CD pipelines and avoid Docker Hub rate limiting issues. I’ll explore the problem, the solution architecture, and provide complete Kubernetes manifests for production use.
What is Dagger?
Dagger is a modern CI/CD engine that allows you to write your pipelines as code. It provides a powerful API for building, testing, and deploying applications. Dagger Engine runs as a service that can be shared across multiple CI/CD pipelines, providing caching and performance benefits.
The Problem: Docker Hub Rate Limiting
When running CI/CD pipelines with Dagger, each build typically pulls multiple Docker images from Docker Hub:
- Base images:
golang:1.21
,node:18
,python:3.11
- Build tools:
alpine:latest
,ubuntu:20.04
- Custom images: Your application images
The Challenge:
- Docker Hub has rate limits (200 pulls per 6 hours for anonymous users)
- Each CI/CD run pulls the same images repeatedly
- Build times increase as images are downloaded each time
- Rate limiting causes pipeline failures
The Solution: Docker Registry Mirror
Instead of pulling images directly from Docker Hub, we’ll deploy a local Docker registry that acts as a mirror/cache:
Architecture
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ CI/CD Pipeline│ │ Dagger Engine │ │ Docker Registry │
│ │ │ │ │ Mirror │
│ - GitHub Actions│───▶│ - BuildKit │───▶│ - Local Cache │
│ - Self-hosted │ │ - Container │ │ - 50GB Storage │
│ runners │ │ Engine │ │ - Proxy to │
└─────────────────┘ └──────────────────┘ │ Docker Hub │
└─────────────────┘
│
▼
┌─────────────────┐
│ Docker Hub │
│ (External) │
└─────────────────┘
Benefits
- Automatic Caching: First pull caches the image locally
- Subsequent Pulls: Served from local cache, no Docker Hub calls
- Persistent Storage: Images survive pod restarts
- Transparent: No changes needed in CI/CD pipelines
- Rate Limit Protection: Local cache prevents hitting Docker Hub limits
Implementation
Prerequisites
- Kubernetes cluster with containerd runtime
- Persistent storage available (50GB recommended)
- Privileged containers enabled
- Network policies allowing internal communication
1. Persistent Volume for Registry
First, create a persistent volume for the Docker registry cache:
apiVersion: v1
kind: PersistentVolume
metadata:
name: k8s-worker-docker-registry
spec:
capacity:
storage: 50Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /var/lib/k8s/docker
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k8s-worker
2. Docker Registry Deployment
Deploy the Docker registry with proxy configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-registry
namespace: docker
spec:
replicas: 1
selector:
matchLabels:
app: docker-registry
template:
metadata:
labels:
app: docker-registry
spec:
containers:
- name: registry
image: registry:2
ports:
- containerPort: 5000
env:
- name: REGISTRY_PROXY_REMOTEURL
value: "https://registry-1.docker.io"
volumeMounts:
- name: registry-storage
mountPath: /var/lib/registry
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
volumes:
- name: registry-storage
persistentVolumeClaim:
claimName: docker-registry
3. Dagger Engine Configuration
Configure Dagger Engine to use the local registry through BuildKit:
Note: Dagger Engine uses BuildKit directly for image operations, not the system containerd. Therefore, we configure BuildKit registry mirrors rather than containerd configuration.
apiVersion: v1
kind: ConfigMap
metadata:
name: dagger-buildkit-config
namespace: dagger
data:
buildkitd.toml: |
debug = true
[registry."docker.io"]
mirrors = ["http://registry.docker.svc.cluster.local:5000"]
[registry."ghcr.io"]
mirrors = ["http://registry.docker.svc.cluster.local:5000"]
Mount this configuration in the Dagger Engine:
volumeMounts:
- name: buildkit-config
mountPath: /etc/buildkit
volumes:
- name: buildkit-config
configMap:
name: dagger-buildkit-config
4. Complete Deployment
Deploy all components:
# Deploy Docker registry
kubectl apply -f deployments/docker-registry/
# Deploy Dagger Engine
kubectl apply -f deployments/dagger-engine/
# Verify deployment
kubectl get pods -n docker
kubectl get pods -n dagger
Performance Benefits
Before (Direct Docker Hub)
- First build: Slow (download images from Docker Hub)
- Subsequent builds: Slow (re-download same images)
- Rate limiting: Pipeline failures after 200 pulls
- Network usage: High bandwidth consumption
After (Registry Mirror)
- First build: Same speed (download and cache images)
- Subsequent builds: Much faster (serve from local cache)
- Rate limiting: Eliminated (local cache)
- Network usage: Minimal (only first-time downloads)
GitHub Actions Integration
Before: Traditional Dagger Usage
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Run Dagger
run: |
dagger call lint --source-dir .
dagger call test --source-dir .
This approach:
- Pulls images for each step
- No caching between builds
- Hits Docker Hub rate limits
- Slower build times
After: Using Self-Hosted Dagger Engine
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: k8s-home-runners
steps:
- uses: actions/checkout@v4
- name: Run Dagger with Engine
env:
_EXPERIMENTAL_DAGGER_RUNNER_HOST: "tcp://dagger-engine.dagger.svc.cluster.local:8080"
run: |
dagger call lint --source-dir .
dagger call test --source-dir .
This approach:
- Reuses cached images
- Faster build times
- No Docker Hub rate limit issues
- Consistent performance
Monitoring and Maintenance
Check Registry Cache
# Check registry storage usage
kubectl exec -n docker deployment/docker-registry -- du -sh /var/lib/registry
# List cached images
kubectl exec -n docker deployment/docker-registry -- find /var/lib/registry -type d | grep -v "^/var/lib/registry$" || echo "No cached images yet"
Registry Health
# Check registry status
kubectl get pods -n docker
kubectl logs -n docker deployment/docker-registry
# Test registry connectivity
kubectl exec -n dagger deployment/dagger-engine -- wget -qO- http://registry.docker.svc.cluster.local:5000/v2/
Dagger Engine Status
# Check engine status
kubectl get pods -n dagger
kubectl logs -n dagger deployment/dagger-engine
# Check BuildKit configuration
kubectl exec -n dagger deployment/dagger-engine -- cat /etc/buildkit/buildkitd.toml
Security Considerations
- Internal Only: Registry is accessible only within the cluster
- No Authentication: Simple setup for local use
- Network Policies: Can be added for additional security
- Storage: 50GB persistent storage for image cache
Conclusion
This solution provides a robust way to speed up CI/CD pipelines while avoiding Docker Hub rate limits. The Docker registry mirror acts as an intelligent cache layer, automatically storing frequently used images and serving them locally.
Key Benefits:
- Eliminates Docker Hub rate limiting
- Speeds up builds significantly
- No changes required in CI/CD pipelines
- Persistent cache survives pod restarts
- Transparent to users
The combination of Dagger Engine for build orchestration and Docker registry mirror for image caching creates a powerful, self-contained CI/CD infrastructure that’s both fast and reliable.
comments powered by Disqus