Swacblooms🦋

Making the Moves
Menu
  • Home
  • Motivation
  • Education
  • Programming
  • About
  • Contact
  • Privacy Policy
Home
Programming
Migrating from Cloudflare R2 to Garage in .NET
Programming

Migrating from Cloudflare R2 to Garage in .NET

Samson Amaugo December 21, 2025

I recently migrated my .NET side project’s object storage from Cloudflare R2 to Garage (self-hosted S3-compatible storage). The main driver was cost predictability – I wanted fixed infrastructure costs instead of variable charges.

Here’s how I configured it.

Garage Bucket-to-Website

Garage doesn’t currently support anonymous S3 access, so I used the bucket-to-website feature for public file access. This works by:

1. Setting the bucket name to match my public domain (e.g., cdn.myapp.com)

2. Enabling website mode via the Garage web UI

3. Pointing my DNS to Garage’s web endpoint

4. Uploading files via S3 API

5. Accessing files directly via public HTTP URLs on my domain

This provides persistent public access without pre-signed URLs – perfect for truly public assets like logos.

Environment Variables

Before (Cloudflare R2):

CLOUDFLARE_R2_ENDPOINT

CLOUDFLARE_R2_ACCESS_KEY_ID

CLOUDFLARE_R2_SECRET_ACCESS_KEY

CLOUDFLARE_R2_BUCKET_NAME

After (Garage):

GARAGE_S3_ENDPOINT=https://cdn.myapp.com # Public web endpoint (matches bucket name)

GARAGE_S3_API_ENDPOINT=https://s3-api.myapp.com # S3 API endpoint (optional)

GARAGE_S3_ACCESS_KEY_ID=your_key_id

GARAGE_S3_SECRET_ACCESS_KEY=your_secret

GARAGE_S3_BUCKET_NAME=cdn.myapp.com # Must match public domain for website mode

Important: The bucket name must match your public domain for bucket-to-website to work.

S3 Client Configuration

The critical configuration for Garage compatibility:

var config = new AmazonS3Config

{

   ServiceURL = apiServiceUrl, // Garage S3 API endpoint

   ForcePathStyle = true, // Required for Garage

   AuthenticationRegion = "garage"

};

var s3Client = new AmazonS3Client(accessKeyId, secretAccessKey, config);

Key points:

– ForcePathStyle = true is required for Garage

– AuthenticationRegion = “garage” This is particularly important for non-AWS S3-compatible services like Garage that use custom region naming

– Separate API endpoint for uploads vs. public website endpoint for downloads

Upload Implementation

public async Task<string> UploadImageAsync(Stream fileStream, string fileName)

{

   var uniqueFileName = $"logos/{Guid.NewGuid()}{Path.GetExtension(fileName)}";

   var putRequest = new PutObjectRequest

   {

      BucketName = _bucketName,

      Key = uniqueFileName,

      InputStream = fileStream,

      ContentType = contentType,

      UseChunkEncoding = false // Important for compatibility

   };

   await _s3Client.PutObjectAsync(putRequest);

   // Return public website URL

   return $"{_publicEndpoint}/{uniqueFileName}";

}

The returned URL (https://cdn.myapp.com/logos/abc123.png) is directly accessible – no auth needed.

Migrating Existing Assets

I used a simple script to transfer existing files from R2 to Garage. The script downloaded files from R2 and re-uploaded them to Garage using the S3 API, preserving the same file paths for zero downtime.

Key Takeaways

Critical configuration:

– ForcePathStyle = true and AuthenticationRegion = “garage” are essential

– UseChunkEncoding = false prevents upload issues

– Bucket name must match your public domain for website mode

– DNS must point to Garage’s web endpoint

Why bucket-to-website:

– Garage doesn’t support anonymous S3 access yet

– Provides persistent public access without pre-signed URLs

– Perfect for truly public assets (logos, images, etc.)

Why Garage:

– Fixed costs instead of variable API/bandwidth charges

– Full control over storage infrastructure

– S3 is compatible with minimal code changes

For side projects where cost predictability matters, Garage is excellent. Self-hosting means I know exactly what I’m paying each month.

—

Resources:

– Garage Documentation

– AWSSDK.S3 NuGet Package

Prev Article
Next Article

Related Articles

Making a CRUD API using Azure Functions
I wrote a post that illustrates how to make a …

Making a CRUD API using Azure Functions

Attributes in C#
Yello, so in today’s article, I would be writing about …

Creating Attributes and Using Attributes in C#

About The Author

Samson Amaugo

I am Samson Amaugo. I am a full-stack developer and I specialize in DotNet and the MERN stack.

Search Site

Recent Posts

  • Cloudflare Tunnel to Postgres, plus Swarm’s static IP problem
  • The Topological Sort
  • Building a Perceptron in .NET
  • Part 4 — Assertions: Verifying What You See
  • Part 3 — Actions and Auto-Waiting: Putting Locators to Work

Categories

  • EDUCATION
  • Motivation
  • Programming
  • Uncategorized

Get more stuff

Subscribe to our mailing list and get interesting stuff and updates to your email inbox.

Thank you for subscribing.

Something went wrong.

we respect your privacy and take protecting it seriously

RSS feed: Swacblooms Swacblooms

Swacblooms🦋

Making the Moves
Copyright © 2026 Swacblooms🦋
Swacblooms - Making the Moves