How Blacksmith Accelerates Docker Builds in CI/CD Pipelines
How Blacksmith Accelerates Docker Builds in CI/CD Pipelines
Introduction
Every developer knows the frustration: you push a minor code change, trigger your GitHub Actions workflow, and then wait. And wait. Your Docker build churns through layers it’s already built before, npm reinstalls packages that haven’t changed, and what should take seconds stretches into minutes. For teams deploying multiple times daily, these slow builds don’t just waste time—they break flow, delay feedback, and compound across every pull request.
Blacksmith emerged as a solution to this exact problem. By providing intelligent, persistent caching infrastructure specifically designed for GitHub Actions, Blacksmith transforms CI/CD pipeline performance. In this post, we’ll explore how Blacksmith works, why traditional caching falls short, and how to integrate it into your workflows for dramatic build time improvements.
The CI/CD Caching Problem
Before diving into Blacksmith, let’s understand why GitHub Actions builds are inherently slow.
Why GitHub Actions Builds Are Slow
GitHub Actions runners are ephemeral—they spin up fresh for each job, execute your workflow, and terminate. This stateless design ensures consistency and security, but creates a significant performance bottleneck: every build starts from scratch.
Consider a typical Docker build workflow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ Problematic: No persistent cache
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t myapp:latest .
- name: Push to registry
run: docker push myapp:latest
What happens on each run:
- Fresh Ubuntu runner provisions (~20-30 seconds)
- Docker pulls base images from scratch (1-2 minutes)
- All layers rebuild, even unchanged dependencies (2-5 minutes)
- Total time: 3-7 minutes per build
For a team with 20 developers pushing 5 times daily, that’s 300-700 minutes of waiting every single day.
Traditional Caching Limitations
GitHub Actions provides built-in caching via actions/cache, but it has significant limitations:
1
2
3
4
5
6
7
# ⚠️ Limited: GitHub Actions cache
- uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: $-buildx-$
restore-keys: |
$-buildx-
Limitations:
- Size restrictions: 10GB cache limit per repository
- Slow restore: Cache downloads compete with other network operations
- Eviction policies: Caches older than 7 days are automatically purged
- Network overhead: Fetching large caches from GitHub’s storage adds latency
For large monorepos or projects with heavy dependencies (Node.js with thousands of packages, large Python environments), these constraints make traditional caching insufficient.
What Is Blacksmith?
Blacksmith is a managed caching infrastructure that sits between your GitHub Actions runners and your build processes. Think of it as a CDN for your build artifacts—intelligently positioned, blazingly fast, and purpose-built for CI/CD workloads.
Core Architecture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────┐
│ GitHub Actions │
│ Runner │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌──────────────┐
│ Blacksmith │────▶│ Docker Hub │
│ Cache Layer │ │ NPM Registry │
└─────────────────┘ │ RubyGems │
│ └──────────────┘
▼
┌─────────────────┐
│ Fast SSD/NVMe │
│ Persistent │
│ Cache Storage │
└─────────────────┘
Key Components:
- High-speed cache nodes: Geographically distributed for low latency
- Smart invalidation: Understands semantic versioning and dependency graphs
- Transparent proxying: Works with existing Docker, npm, pip, gem commands
- Intelligent warming: Predictively caches based on usage patterns
How Blacksmith Works
1. Layer-Aware Docker Caching
Blacksmith understands Docker’s layer architecture and caches each layer independently with content-addressable storage.
Before Blacksmith:
1
2
3
4
5
6
7
# Every build pulls everything fresh
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install # ⏱️ 2-3 minutes every time
COPY . .
RUN npm run build # ⏱️ 1-2 minutes
With Blacksmith:
1
2
3
4
5
6
FROM node:18-alpine # ✅ Cached: 5 seconds
WORKDIR /app
COPY package*.json ./
RUN npm install # ✅ Cached: 10 seconds (if package.json unchanged)
COPY . .
RUN npm run build # ⏱️ Only this runs: 1-2 minutes
Blacksmith caches:
- Base image layers (
node:18-alpine) - Dependency installation layers (
npm installoutput) - Build artifacts (when layer hasn’t changed)
Result: Build time drops from 4-5 minutes to 1-2 minutes on typical iterations.
2. Package Registry Proxying
Blacksmith acts as a transparent caching proxy for package registries.
1
2
3
4
5
6
# Without Blacksmith: Downloads from public registry
npm install express # Fetches from npmjs.com
# With Blacksmith: Proxied and cached
npm install express # First time: fetch + cache
npm install express # Subsequent: instant from cache
This works for:
- npm/yarn (Node.js)
- pip (Python)
- gem (Ruby)
- Maven/Gradle (Java)
- NuGet (.NET)
3. Intelligent Cache Warming
Blacksmith analyzes your repository’s dependency patterns and pre-warms caches before builds even start.
Example workflow:
- You push code at 9:00 AM
- Blacksmith detects
package.jsonchanged - Background process pre-fetches new dependencies
- When GitHub Actions runner starts at 9:01 AM, cache is ready
- Build begins with zero cold-start penalty
Integrating Blacksmith into GitHub Actions
Step 1: Initial Setup
First, sign up for Blacksmith and obtain your cache endpoint URL and authentication token.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# .github/workflows/build.yml
name: Build with Blacksmith
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configure Blacksmith
env:
BLACKSMITH_TOKEN: $
run: |
# Configure Docker to use Blacksmith registry mirror
mkdir -p ~/.docker
cat > ~/.docker/daemon.json <<EOF
{
"registry-mirrors": ["https://cache.blacksmith.sh"],
"insecure-registries": ["cache.blacksmith.sh"]
}
EOF
Step 2: Docker Build with Blacksmith
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
driver-opts: |
image=moby/buildkit:latest
network=host
- name: Build with cache
uses: docker/build-push-action@v4
with:
context: .
push: false
tags: myapp:latest
cache-from: type=registry,ref=cache.blacksmith.sh/myapp:buildcache
cache-to: type=registry,ref=cache.blacksmith.sh/myapp:buildcache,mode=max
Key parameters:
cache-from: Pull cache layers from Blacksmithcache-to: Push new cache layers back to Blacksmithmode=max: Cache all layers, not just final image
Step 3: Optimizing Multi-Stage Builds
For complex applications, use multi-stage builds to maximize caching efficiency:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Stage 1: Dependencies (rarely changes)
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Build (changes frequently)
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production (minimal)
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
Cache behavior:
depsstage: Cached untilpackage.jsonchangesbuilderstage: Rebuilds only when source code changesrunnerstage: Reconstructs from cached stages
Performance impact:
- Cold build (no cache): 8-10 minutes
- Warm build (code change only): 2-3 minutes
- Hot build (no changes): 30-45 seconds
Step 4: NPM/Yarn Caching
Configure npm to use Blacksmith as a registry proxy:
1
2
3
4
5
6
7
8
9
10
- name: Setup Node.js with Blacksmith
uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://npm.blacksmith.sh'
env:
NODE_AUTH_TOKEN: $
- name: Install dependencies
run: npm ci
Before: npm ci takes 90-120 seconds
After: npm ci takes 15-20 seconds on cache hit
Real-World Performance Benchmarks
Case Study: Medium-Sized Node.js Application
Setup:
- Express.js API with 150 dependencies
- PostgreSQL integration tests
- Docker multi-stage build
- 3 environments (dev, staging, prod)
Build times without Blacksmith:
- Clean build: 6m 45s
- Incremental build: 4m 20s
- Cache hit rate: ~30%
Build times with Blacksmith:
- Clean build: 2m 10s (68% faster)
- Incremental build: 1m 15s (71% faster)
- Cache hit rate: ~85%
Cost impact:
- GitHub Actions minutes saved: ~75% reduction
- For 100 builds/week: 300 minutes saved → $15-30/month in runner costs
- Blacksmith cost: $49/month
- ROI: Positive after ~150 builds/month, plus developer time saved
Case Study: Large Monorepo
Setup:
- Turborepo with 12 TypeScript packages
- Shared component library
- End-to-end Playwright tests
- Multiple Docker services
Build times without Blacksmith:
- Full build: 18m 30s
- Partial rebuild: 8m 45s
- Test suite: 12m 15s
Build times with Blacksmith:
- Full build: 5m 20s (71% faster)
- Partial rebuild: 2m 10s (75% faster)
- Test suite: 3m 45s (69% faster)
Developer impact:
- 40 developers × 5 pushes/day = 200 builds/day
- Time saved per day: 2,640 minutes (44 hours)
- Annual developer productivity gain: ~$180,000 (at $75/hour blended rate)
Common Pitfalls and Solutions
Pitfall 1: Cache Invalidation Issues
Problem: Stale dependencies cause runtime errors despite successful builds.
1
2
3
# ❌ Bad: Broad COPY invalidates cache unnecessarily
COPY . .
RUN npm install
Solution: Copy dependency manifests first, install, then copy source.
1
2
3
4
# ✅ Good: Precise cache invalidation
COPY package*.json ./
RUN npm ci
COPY . .
Pitfall 2: Ignoring .dockerignore
Problem: Including node_modules, .git, or build artifacts in context invalidates cache.
# .dockerignore
node_modules
.git
.github
dist
*.log
.env
Impact: Context size drops from 500MB to 10MB, improving cache performance.
Pitfall 3: Not Using BuildKit
Problem: Legacy Docker builder has poor layer caching.
1
2
3
4
5
# ✅ Always enable BuildKit
- name: Build with BuildKit
env:
DOCKER_BUILDKIT: 1
run: docker build -t myapp:latest .
Pitfall 4: Overly Aggressive Cache Keys
Problem: Cache never hits because key is too specific.
1
2
3
4
5
# ❌ Bad: SHA changes every commit
cache-key: $
# ✅ Good: Hash of dependency files
cache-key: $
When to Use Blacksmith
✅ Ideal Use Cases
- High-frequency deployments: Teams pushing 10+ times daily
- Large dependency trees: Node.js, Python, or Java projects with 100+ packages
- Docker-heavy workflows: Microservices, containerized applications
- Monorepos: Multiple packages sharing common dependencies
- Distributed teams: Consistent cache performance across regions
⚠️ May Not Be Worth It
- Simple static sites: Jekyll, Hugo builds with minimal dependencies
- Infrequent builds: Less than 5 builds per week
- Small projects: Single-file scripts or minimal dependency projects
- Cost-sensitive hobbyists: Free GitHub Actions tier is sufficient
Advanced: Blacksmith + BuildKit Inline Cache
For ultimate performance, combine Blacksmith with BuildKit’s inline cache export:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name: Build and push with inline cache
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
myregistry/myapp:$
myregistry/myapp:latest
cache-from: |
type=registry,ref=cache.blacksmith.sh/myapp:buildcache
type=registry,ref=myregistry/myapp:latest
cache-to: type=inline
build-args: |
BUILDKIT_INLINE_CACHE=1
This embeds cache metadata directly in the image, allowing any runner to benefit from previous builds without additional cache storage.
Monitoring and Optimization
Measuring Cache Effectiveness
Add metrics to your workflow:
1
2
3
4
- name: Analyze cache performance
run: |
echo "Cache hit rate: $(docker buildx du | grep 'shared' | awk '{print $2}')"
echo "Total cache size: $(docker buildx du | grep 'Total' | awk '{print $2}')"
Blacksmith Dashboard Metrics
Monitor through Blacksmith’s dashboard:
- Cache hit rate: Target >80% for optimal performance
- Bandwidth saved: Track data not downloaded from public registries
- Build time reduction: Compare against baseline
- Regional performance: Ensure cache nodes are serving your runners efficiently
Conclusion
Blacksmith transforms GitHub Actions from a bottleneck into a competitive advantage. By intelligently caching Docker layers, proxying package registries, and pre-warming dependencies, it cuts build times by 60-80% for typical workflows. For teams serious about CI/CD performance, the combination of faster feedback loops, reduced runner costs, and improved developer experience makes Blacksmith a compelling investment.
The key insight: CI/CD caching isn’t just about storing files—it’s about understanding dependency semantics, layer relationships, and workflow patterns. Blacksmith’s architecture reflects this understanding, delivering performance gains that generic caching solutions simply can’t match.
Start with your slowest builds. Measure baseline performance. Integrate Blacksmith. Watch your pipeline transform from “let’s grab coffee” to “already done.”
Suggested Reading
- Docker Build Best Practices - Official Docker Documentation
- GitHub Actions Caching Documentation
- BuildKit Documentation
- The Twelve-Factor App: Build, Release, Run
- Docker Layer Caching: How it Works - Docker Blog
- Optimizing CI/CD Pipelines - Martin Fowler on Continuous Integration