Skip to main content
Back to Blog
Dev

Migrating to Cloudflare R2: Moving Images Out of the Codebase

December 13, 2025·7 min read

For a while, I just kept image files inside the project. Psychology test result images, game thumbnails, various icons — they quietly piled up in the `public/images` folder until the repository size started to feel heavy. Cloning took noticeably longer and the CI/CD pipeline felt sluggish for no good reason. I told myself I'd separate them eventually, kept putting it off, and finally just did it.

Why R2: Cost Comparison and the Same Ecosystem

The candidates that came to mind were AWS S3, Cloudinary, and Cloudflare R2. I had a rough idea about each, but since I had never actually used them, I had no clear sense of the costs or limits. So I asked Gemini again. I asked it to summarize the pros and cons and approximate pricing for each service, and the conclusion was that R2 is the cheapest option for a small-scale side project.

On top of that, I was already deploying with Cloudflare Pages. Managing everything from the same Cloudflare dashboard seemed like it would flow naturally, and serving content within the same network seemed like it would reduce latency too. The decision was easy.

Setup and Migration: Screenshots, AI, and a Script

From creating the R2 bucket to setting up the public domain, I followed the same approach as in episode 3 (Cloudflare Pages) and episode 5 (Firebase) — take a screenshot of the dashboard and ask Gemini "what do I do next here?" one step at a time. With this method, even a service you've never used before gets configured without getting stuck. Once again I felt that with AI around, almost anything is doable.

The problem was actually moving the images. There were quite a few files in `public/images`, and uploading them one by one would have taken forever. I asked Gemini to write a bulk upload script, and it made one that used the R2 API to upload everything while preserving the folder structure. I then asked it to handle the batch replacement of hardcoded image paths in the codebase with R2 URLs as well.

Unexpected Problems

The migration itself finished faster than expected. But there was some cleanup work afterward. The first issue was images not showing up in the local development environment. The cause was that `localhost` was missing from the R2 CORS policy. Adding `localhost` to the CORS policy fixed it.

The second issue was a CORS cache problem in the psychology test result image save feature. The function that saves the result card as an image suddenly started throwing errors after the R2 migration. What made it tricky was that the browser had cached the old CORS response, so even after updating the config, the error kept appearing until the cache expired. AI helped identify the cause and resolve it.

The image files are still sitting in the git history. There are ways to clean it up, but it risks breaking other commits if done carelessly, so I left it alone. It bothers me a little, but that's just how it is.

The Project Got Lighter

The first thing I noticed after migrating to R2 was the clone speed. With image files out of the repository, the total size dropped noticeably. CI/CD felt a bit lighter too. It might be a small difference, but not having to wait unnecessarily for images during development was clearly more comfortable.

I Wish I Had Used R2 from the Start

If I had stored images in an external service from the start, the git history would be much cleaner. But I was using Cloudflare for the first time, and I could not make a clear judgment about where to go for storage at first. I needed to actually run the side project for a while and experience the cost structure firsthand before a real comparison was possible — so not having chosen R2 from the start was just unavoidable.

Moving to R2 now and lightening the project was still the right call. I've decided to think of it as just how side projects work — you build and fix as you go. The images left in the history are just a trace of how things were back then.

If I start a new project next time, I'll probably separate image storage from day one. This experience at least taught me that much.

More Posts