The-DevOps-Daily / The-DevOps-Daily/devops-daily

Migrate images to Cloudflare R2 to reduce file count and improve scalability

Open
#557 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

chore enhancement
Dominant language
TypeScript
Stars
1.1k
Forks
392
Avg merge
5h 8m
Merged PRs (30d)
121

Description

🎯 Problem

Currently, all generated images (SVG + PNG) are committed to the repository and deployed with the site, counting toward Cloudflare Pages' 20,000 file limit.

Current state:

  • Total files: ~11,095 (55% of limit)
  • Image files: ~989 (495 SVG + 494 PNG)
  • Headroom: ~8,905 files
  • Projected limit: ~1,500 posts before hitting 20k limit

Why this matters:

  • As content grows (posts, guides, news), we'll hit the file limit
  • Each new post adds 2 files (SVG + PNG)
  • At current growth rate, we'll hit limit within 1-2 years

💡 Proposed Solution

Migrate generated images to Cloudflare R2 (S3-compatible object storage) instead of committing them to the repository.

Benefits:
  1. Scalability

    • Images don't count toward 20k file limit
    • Can grow to 4,000+ posts without file concerns
    • Removes ~1,000 files from deployments
  2. Performance

    • R2 served via Cloudflare CDN globally
    • Can enable automatic WebP/AVIF optimization with Cloudflare Images
    • No egress fees (free bandwidth on Cloudflare network)
  3. Cost

    • R2 free tier: 10GB storage, 1M read requests/month
    • Current images: ~16MB
    • Estimated cost: $0/month (well within free tier)
  4. Flexibility

    • Easier to update/purge individual images
    • Can serve optimized formats per browser
    • Separates static content from code deployments
Implementation Strategy:
Phase 1: Setup R2 Bucket
wrangler r2 bucket create devops-daily-images
Phase 2: Create Upload Script
// scripts/upload-to-r2.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import glob from 'fast-glob';

const r2 = new S3Client({
  region: 'auto',
  endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  },
});

async function uploadImages() {
  const images = await glob('public/images/**/*.{png,svg}');
  
  for (const imagePath of images) {
    const key = imagePath.replace('public/', '');
    const fileContent = fs.readFileSync(imagePath);
    
    await r2.send(new PutObjectCommand({
      Bucket: 'devops-daily-images',
      Key: key,
      Body: fileContent,
      ContentType: imagePath.endsWith('.svg') ? 'image/svg+xml' : 'image/png',
      CacheControl: 'public, max-age=31536000, immutable',
    }));
  }
}
Phase 3: Configure Custom Domain
  • Connect R2 bucket to custom domain: images.devops-daily.com
  • Cloudflare automatically handles CDN caching
  • No code changes needed (or minimal proxy setup)
Phase 4: Update Build Pipeline
// package.json
{
  "scripts": {
    "upload:r2": "tsx scripts/upload-to-r2.ts",
    "build:cf": "npm run copy:markdown && npm run generate-search-index && npm run generate:images:parallel && npm run upload:r2 && next build && npm run copy:markdown:out"
  }
}
Phase 5: Update Image URLs (Option A: Direct)
// lib/image-utils.ts
const IMAGE_CDN = process.env.NEXT_PUBLIC_IMAGE_CDN || 'https://images.devops-daily.com';

export function getPostImagePath(slug: string, type: string = 'posts'): string {
  return `${IMAGE_CDN}/images/${type}/${slug}.svg`;
}
Phase 5: Update Image URLs (Option B: Proxy - No Code Changes)
// Cloudflare Worker at /_worker.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    
    // Proxy /images/* to R2
    if (url.pathname.startsWith('/images/')) {
      const r2Object = await env.R2_BUCKET.get(url.pathname.slice(1));
      if (r2Object) {
        return new Response(r2Object.body, {
          headers: {
            'Content-Type': r2Object.httpMetadata?.contentType || 'image/png',
            'Cache-Control': 'public, max-age=31536000, immutable',
          },
        });
      }
    }
    
    return env.ASSETS.fetch(request);
  }
};

🎯 Success Criteria

  • R2 bucket created and configured
  • Upload script working in CI/CD
  • Custom domain images.devops-daily.com connected
  • Images served from R2 instead of static deployment
  • File count reduced by ~1,000 files
  • All pages load correctly with R2 images
  • Social sharing (Twitter, LinkedIn, Daily.dev) working with PNG images
  • No performance regression (check PageSpeed Insights)

📊 Expected Impact

Metric Before After Improvement
Total files 11,095 10,100 -995 files
Image files 989 0 -989 files
Headroom 8,905 9,900 +995 files
Can grow to ~1,500 posts ~4,000 posts +167%
Cost $0 $0 $0 (within free tier)

🔗 References

🚀 Alternative Approaches Considered

  1. Stop generating SVG - Saves ~500 files but limits scalability
  2. On-demand image generation - Complex, requires Cloudflare Workers
  3. Use external service - Costs money, adds dependency

⚠️ Risks & Mitigations

Risk Mitigation
R2 downtime Cloudflare has 99.99% SLA; images cached at CDN edge
Migration complexity Use proxy worker to avoid code changes initially
Cost overruns Set up billing alerts; current usage well within free tier
Build time increase Upload to R2 in parallel; ~10-20s additional time

📝 Implementation Checklist

  • Create R2 bucket
  • Generate R2 API credentials
  • Implement upload script
  • Test upload locally
  • Configure custom domain
  • Update build pipeline
  • Test in Cloudflare Pages preview
  • Deploy to production
  • Monitor for issues
  • Clean up local image files from repo (optional)

💬 Discussion

Questions to resolve:

  • Should we keep local copies of images in public/images as fallback?
  • Should we migrate existing images or only new ones?
  • Do we want to enable Cloudflare Images transforms (WebP/AVIF)?
  • Should we set up separate buckets for different content types?

Priority: Medium
Effort: 2-3 days
Impact: High (unblocks long-term growth)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the proposed scripts/upload-to-r2.ts, package.json build scripts, lib/image-utils.ts, and the public/images directory. Trace how images are generated and referenced, then validate the upload flow in Cloudflare Pages preview. Done means images are served from R2, pages and social previews still work, and the deployment file count is reduced.

Written by the indexing model from the issue text.

Assessment

Tech stack
cloud, typescript
Domain
build-system, cloud, devops, web-dev
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.