πŸš€ Speed Up Your Images: Complete Guide to Cloudflare CDN + Amazon S3
Back to Blog

πŸš€ Speed Up Your Images: Complete Guide to Cloudflare CDN + Amazon S3

β€’6 min read
DevOps

So here's the thing, I had this Rails app where images were taking forever to load. Users were complaining, and my AWS bill was climbing. After some research, I set up Cloudflare CDN in front of my S3 bucket, and wowβ€”the difference was night and day.

Why This Actually Works

Before I get into the how-to, let me quickly explain why this setup is so effective:

When you serve images directly from S3, every single request hits your bucket. A user in Tokyo requesting an image from your US-East bucket? That's a long round trip. A user refreshing the page? Another S3 request and another charge on your AWS bill.

With Cloudflare in front, images get cached at edge locations worldwide. That Tokyo user gets the image from Cloudflare's Tokyo datacenter instead of Virginia. Plus, once an image is cached, you're not paying S3 bandwidth costs for repeated requests.

What You'll Need:

  • An AWS account with an S3 bucket (or willingness to create one)
  • A domain on Cloudflare (free tier works fine)
  • About 30 minutes

Step 1: Setting Up Your S3 Bucket

First, create a new S3 bucket or use an existing one. I called mine my-app-images-2024 but you can name it whatever makes sense.

The tricky part: you need to turn off Block All Public Access.

Choose your region based on where most of your users are, or just pick the one closest to you.

Step 2: The Bucket Policy

You need to add a bucket policy to actually make the images readable. This is the JSON you need to paste in (replace my-images-bucket with your actual bucket name):

text
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPublicRead",
      "Effect": "Allow",
      "Principal": "*",
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::my-images-bucket/*"]
    }
  ]
}

Go to your bucket β†’ Permissions β†’ Bucket Policy, paste this in, and save. After this, you should be able to access any image in your bucket directly:

text
https://my-images-bucket.s3.amazonaws.com/path/to/image.jpg

Note: Test this before moving on - upload a test image and try accessing it via the URL.

Step 3: Cloudflare CNAME Setup

Now for the good stuff. In your Cloudflare dashboard:

  1. Go to DNS
  2. Add a CNAME record
  3. Set the name to something like images or cdn
  4. Point it to your S3 bucket: my-images-bucket.s3.amazonaws.com
  5. Make sure the cloud is orange (proxied through Cloudflare) 🟠

Now your images are accessible at https://images.yourdomain.com/path/to/image.jpg instead of the ugly raw S3 URL.

Step 4: Caching Rules (Where the Magic Happens)

This is where you actually get the performance benefits. Without proper caching rules, Cloudflare might not cache your images aggressively.

Go to Rules β†’ Cache Rules and create a new Cache rule:

  • Select Cache everything Template
  • Edge Cache TTL: 1 month (images don't change often)
  • Browser Cache TTL: 7 days
  • URL pattern: images.yourdomain.com/*

I initially set this to cache for just a few hours, but realized that was stupid - images rarely change, so why not cache them for a month?

Step 5: Cloud Connector (Don't Skip This!)

Okay, I need to be honest - I initially thought Cloud Connector was just extra fluff, but it's actually really important. It handles the technical details that make S3 work properly with Cloudflare.

Here's what Cloud Connector does automatically:

  • Fixes the Host header - S3 is picky about headers, and this makes sure they match what your bucket expects
  • Routes traffic correctly - Makes sure requests actually reach your bucket

Go to Rule β†’ Cloud Connector and click Create Cloud Connector.

Select AWS S3 since that's what we're using.

For the hostname pattern, use a wildcard like images.yourdomain.com/* since you'll be serving different images from various paths.

Pro tip: Double-check that your bucket name matches exactly what you put in the connector. I spent 20 minutes debugging why my images weren't loading, only to realize I had a typo in the bucket name. πŸ€¦β€β™‚οΈ

Step 6: Rails Helper (If You're Using Rails)

Here's the Rails helper I wrote to make this work seamlessly with ActiveStorage:

text
module ApplicationHelper
  include Pagy::Frontend
  
  def cdn_image_url(attachment_or_blob, **opts)
    blob = attachment_or_blob.is_a?(ActiveStorage::Blob) ? 
           attachment_or_blob : 
           attachment_or_blob&.blob
    return unless blob
    
    base = ENV['CDN_CLOUDFLARE_URL'].presence || 
           Rails.application.routes.url_helpers.rails_blob_url(blob, only_path: false)
    url = "#{base.chomp('/')}/#{blob.key.delete_prefix('/')}"
    
    return url if opts.blank?
    
    params = opts.slice(:width, :height, :quality, :format)
                .map { |k, v| "#{k.to_s[0]}=#{v}" }
    "#{url}?#{params.join('&')}"
  end
end

Then in your views:

text
<%= image_tag cdn_image_url(user.avatar, width: 300, quality: 80, format: :webp) %>

I set the CDN_CLOUDFLARE_URL in my environment variables, so I can easily switch between development (direct S3) and production (Cloudflare CDN).

What I Learned the Hard Way

  1. Cache headers matter. Initially, my images weren't being cached properly because I didn't set up the cache rules correctly.

  2. The first request is still slow. That's expected - Cloudflare has to fetch from S3 the first time. After that, it's lightning fast.

  3. ActiveStorage signed URLs don't work. If you're using Rails, you can't just use the built-in signed URLs because they have query parameters that mess with caching. That's why I built the custom helper.

  4. Test with curl. This command helped me debug caching issues:

text
curl -I https://images.yourdomain.com/some-image.jpg

Look for cf-cache-status: HIT in the response headers.

Worth the Effort?

Absolutely. This took me about 2-3 hours to set up (including debugging time), and the performance improvement was immediate and dramatic. Plus, it's basically free - Cloudflare's free tier handles this just fine for most applications.

If you're serving images directly from S3, you should definitely do this. Your users will thank you, and your AWS bill will too.

References

Read the full article with images on dev.to

Originally published on dev.to

Enjoyed this article? Share it with others!