The Zero-Dollar Infrastructure: Build a Blazing-Fast, Free Image CDN with Cloudflare

Hello everyone,

Let’s cut through the noise. Forget the common tips. Today, we’re going to build something genuinely powerful that you will use constantly.

The Goal: To create our own private, high-performance image hosting service and CDN that costs $0.00. It will be faster and more robust than most paid services, perfect for personal projects, blogs, or even small applications.

We’ll achieve this by combining the free tiers of Cloudflare’s enterprise-grade products: R2 Storage and Cloudflare Workers.

The Architecture: Why This Works

  • Cloudflare R2: This is an object storage solution similar to Amazon S3, but with a crucial difference: there are zero data egress fees. This is the key that makes our project free. We can serve petabytes of data, and Cloudflare won’t charge us for the bandwidth. The free tier includes 10 GB of storage, which is plenty for tens of thousands of images.
  • Cloudflare Workers: These are serverless functions that run on Cloudflare’s global edge network. This means our code (for handling uploads and serving files) will execute in a data center closest to the user, making it incredibly fast. The free tier offers 100,000 requests per day.

We will create a Worker script that acts as a secure gateway. It will handle two things:

  1. Authenticated Uploads: Allow you (and only you) to upload images to your R2 bucket.
  2. Public Serving: Serve those images to the world at lightning speed.

Step 1: Setting Up Your Free R2 Storage Bucket

  1. Log in to your Cloudflare account. If you don’t have one, it’s free to create.
  2. On the left sidebar, go to R2.
  3. Click Create bucket. Give it a unique name (e.g., my-image-cdn-2025). Select a geographic location (it doesn’t matter much since Cloudflare’s network is global).
  4. Once created, click on your new bucket’s name, then go to the Settings tab. Note down the Bucket Name.

Step 2: Creating the Worker

  1. On the left sidebar, go to Workers & Pages.
  2. Click Create application, then Create Worker.
  3. Give your Worker a name (e.g., image-cdn-handler) and click Deploy.

Step 3: The Worker Code (The Brains of the Operation)

Click Edit code on your new Worker. Delete the boilerplate and paste the following JavaScript code. This code is the core of our service.

`JavaScript// A Cloudflare Worker to handle image uploads and serve them from an R2 bucket.

export default {
async fetch(request, env) {
// env object contains our bindings (R2 bucket, secret key).
// These are configured in the Cloudflare dashboard, NOT hardcoded here.

const url = new URL(request.url);
const key = url.pathname.slice(1); // The path after the domain, used as the image key/name.

switch (request.method) {
  // Handles requests to view/serve an image.
  case 'GET':
    if (!key) {
      return new Response('Not Found', { status: 404 });
    }
    
    // Retrieve the image object from our R2 bucket.
    const object = await env.IMAGE_BUCKET.get(key);

    if (object === null) {
      return new Response('Object Not Found', { status: 404 });
    }

    // Set up headers to make the browser cache the image.
    const headers = new Headers();
    object.writeHttpMetadata(headers);
    headers.set('etag', object.httpEtag);
    headers.set('Cache-Control', 'public, max-age=2592000'); // Cache for 30 days

    return new Response(object.body, {
      headers,
    });

  // Handles requests to upload a new image.
  case 'POST':
    // Check for our secret key to authorize the upload.
    const authKey = request.headers.get('X-Auth-Key');
    if (authKey !== env.AUTH_KEY_SECRET) {
      return new Response('Unauthorized', { status: 401 });
    }

    // Put the uploaded image data directly into our R2 bucket.
    await env.IMAGE_BUCKET.put(key, request.body, {
        httpMetadata: request.headers,
    });
    
    // Return the public URL of the uploaded image.
    return new Response(`Successfully uploaded ${key} to: ${url.protocol}//${url.hostname}/${key}`);
    
  default:
    return new Response('Method Not Allowed', {
      status: 405,
      headers: {
        Allow: 'GET, POST',
      },
    });
}

},
};`

Step 4: Connecting the Pieces & Deployment

This is the final and most crucial configuration step.

  1. Bind the R2 Bucket:
  • In your Worker’s settings page, go to Settings > Bindings.
  • Under “R2 Bucket Bindings,” click Add binding.
  • For “Variable name,” type IMAGE_BUCKET (this must match the code: env.IMAGE_BUCKET).
  • Select the R2 bucket you created in Step 1.
  • Click Save.
  1. Set Your Secret Upload Key:
  • Still in the Bindings section, scroll down to “Environment Variables” and click Add variable.
  • For “Variable name,” type AUTH_KEY_SECRET (must match env.AUTH_KEY_SECRET).
  • For “Value,” enter a long, random, secret password. Use a password generator. This is what prevents others from uploading to your service.
  • Click Encrypt to keep it secure, then Save.
  1. Deploy! Go back to your Worker’s main page and click Deploy.

How to Use Your New CDN

Your Worker is now live at your-worker-name.your-subdomain.workers.dev.

  • To Upload an Image (using the command line): Open a terminal and use curl. This command uploads a file named my_cat.jpg and names it cat-picture.jpg on your CDN.Bashcurl -X POST \ --header "X-Auth-Key: YOUR_SECRET_KEY_HERE" \ --data-binary "@my_cat.jpg" \ https://your-worker-name.your-subdomain.workers.dev/cat-picture.jpg
  • To View the Image: Simply navigate to the URL in your browser: https://your-worker-name.your-subdomain.workers.dev/cat-picture.jpg

You have just built a serverless, globally distributed, high-performance image CDN that will cost you nothing. You can even link a custom domain to it for a more professional URL. This is the kind of robust infrastructure that companies pay for, and you built it for free. Enjoy.

3 Likes