Using Backblaze B2 and Cloudflare Workers for free image hosting

When communicating with people online, it’s very common to share images or screenshots of what you’re working on, or even screen recordings, gifs, and other media. Traditionally, you can upload or paste these directly into the applications like Slack or Discord, but if you want to re-share that with another person, the process can be tedious. Alternatively, you could upload the media to a site like Imgur or Gyazo, but these sites are often plagued with advertisements, or compress your content causing quality reduction.

Setting up your own image hosting is very easy, and by following this tutorial, you can do it for free (within some very generous limits). Fun fact: every image you see in this post is hosted using this method.

Introduction

ShareX

ShareX is a fantastic tool for Windows that enables screen capture, file sharing, and much more.

ShareX Example
ShareX Example

ShareX integrates with many services like the previously mentioned Imgur, standard protocols like SFTP, as well as services like Amazon AWS S3 and Backblaze B2. ShareX’s capabilities are extremely extensive so I won’t go into all of its features today, but it’ll be a large component in this tutorial.

Backblaze B2

Backblaze B2 is a cloud storage solution similar to Amazon AWS S3, but at a fraction of the cost. The first 10GB of storage are entirely free, which is what we’ll be using for this tutorial.

Backblaze B2 example dashboard
Backblaze B2 example dashboard

Traditionally, like with a service such as AWS S3, you’d have to pay for bandwidth fees on the content being served which is generally the highest portion of your costs. Thanks to the Bandwidth Alliance though, egress between Backblaze and Cloudflare is entirely free, which we’ll be making heavy use of. If you're interested in learning more about the Bandwidth Alliance, check out Cloudflare's site, or the case studies my company Nodecraft did with both Cloudflare and Backblaze.

Cloudflare

I always have a hard time describing exactly what Cloudflare do, but they, as per their website are “one of the world’s largest networks. Today, businesses, non-profits, bloggers, and anyone with an Internet presence boast faster, more secure websites and apps”. Specifically in this tutorial, we’ll be using their CDN product, DNS, and then additionally Cloudflare Workers for extra cool-ness by rewriting some URLs.

Cloudflare example dashboard
Cloudflare example dashboard

Tutorial

All you need before getting started is an owned domain, and a chosen subdomain you wish to use your file hosting.

Backblaze B2 Bucket Creation

Firstly, you’ll want to register an account for Backblaze’s B2 storage. This will be where we're storing all of our media using their free 10GB of storage. Once registered, from their dashboard's "Buckets" UI, hit "Create a Bucket".

Create a Backblaze B2 Bucket
Create a Backblaze B2 Bucket

Bucket names are unique so you may need to use a little creativity, but for my purposes, I'll be using jross-files. Be sure to set the bucket to public - we want people to be able to see our uploaded screenshots and other media!

With the bucket created, head to the "App keys" section of the UI, and click "Add a New Application Key". This information is what we'll use in ShareX to authenticate with Backblaze and upload our media.

Add Application Key for Backblaze B2 Cloud Storage
Add Application Key for Backblaze B2 Cloud Storage

Give your key a name such as sharex-uploader, and I'd recommend restricting it to your previously created bucket. This isn't required, but is good practice for security. Leave the type of access as "Read and Write", and don't fill in the File name prefix, or Duration as these fields aren't relevant for our use-case.

Successfully generated Backblaze B2 Application Key
Successfully generated Backblaze B2 Application Key

With your key successfully generated, note down the keyID and applicationKey in a secure place such as your password manager. You won't be able to retrieve the applicationKey after this point, so make sure it's saved.

Finally on Backblaze's side, head back to the "Buckets" UI, and click "Upload/Download" on the bucket you just created. Upload a temporary test file to your bucket (such as test.txt) and hit the "i" on the file you just uploaded. This is used to gather some information about where exactly your bucket is hosted for the next steps.

Example uploaded file information in a Backblaze B2 Bucket
Example uploaded file information in a Backblaze B2 Bucket

As the screenshot above shows, our file is hosted at https://f002.backblazeb2.com/ - keep note of this domain as it will be useful in just a moment.

Cloudflare DNS

Next, head on over to Cloudflare and either login to your account, or create a new account. You will need an active domain in your Cloudflare account before continuing, so follow their wizard to get your first domain added to their platform.

With your chosen domain and subdomain, create a DNS CNAME record pointing at the previously discovered fxxx.backblazeb2.com URL.

Cloudflare DNS record for Backblaze B2
Cloudflare DNS record for Backblaze B2

As per the record above, I've chosen files.jross.me for my subdomain, and pointed it towards f002.backblazeb2.com. Ensure that the Cloudflare orange shield is enabled, and the request is proxied. The default Cloudflare auto TTL is fine for our use-case.

Backblaze have a help article on their site which has a few more tips for configuring Cloudflare with Backblaze B2, and some instructions for creating a couple of page rules to ensure your domain can only be used to retrieve files from your bucket. I'd recommend following these instructions before deploying this in production. I'd heavily recommend adding a page-rule to set the "cache level" to "everything", and "edge cache TTL" to a higher value like 7 days if your files aren't often changing.

Test if you can access the file you previously uploaded by visiting https://subdomain.domain.com/file/<bucket-name>/test.txt. If you see your file, you're good to continue!

ShareX Configuration

The following steps will show you how to configure ShareX to upload to your B2 Bucket. As ShareX is Windows only, you may need to find an alternate uploader (or simply use the B2 upload web UI) if you're using another platform.

After installing ShareX, there's a lot of available configuration options that may seem overwhelming initially.

Firstly, let's configure the Backblaze B2 destination with our credentials. From the main ShareX window, click "Destinations -> Destination Settings".

ShareX Backblaze B2 Destination Settings
ShareX Backblaze B2 Destination Settings

Fill in your Application Key ID and Application Key (s

ecret) as we previously saved, and if you didn't specify a specific bucket for your application key, you'll also have to fill in the bucket name.

For the custom URL, you'll want to set this to your subdomain you previously decided, delimited with /file/<bucket-name>, like https://subdomain.domain.com/file/<bucket-name>/. ShareX will fill the rest of the URL as you'll see in the preview box below.

The "Upload path" dictates exactly where screenshots will be saved in B2. In the example above, I'm using %y/%mo/ which will result in files being saved in folders such as /2019/08 for the year and month. There's many other formatters ShareX supports such as randomised folder names, numbers, more specific time formats, etc. but I find that year and month works well for my use-case.

After completing your Backblaze B2 credentials, close out the destinations settings and head back to the main ShareX window. Once again, hover over the "Destinations" menu, but this time, set each of the "Image uploader", "Text uploader", and "File uploader" options to "Backblaze B2", if you wish for all of these media types to land in your B2 bucket.

Testing

Now it's time to use ShareX and upload an image! From the Windows Task Bar, right click on the ShareX icon, click "Capture", and then click "Fullscreen". If you did everything correctly, ShareX should take a few seconds to process your image, and then upload to your B2 Bucket, and return the URL to you.

You can setup hotkeys from within ShareX via the "Hotkey settings" to caption specific regions, monitors, active windows, etc. as well as screen recordings, gif records, and a lot more. I use CTRL + SHIFT + 2 to do a full-screen capture, CTRL + SHIFT + 4 to do a region capture, and then CTRL + SHIFT + 5 to do a region capture, and then edit the image briefly to add highlights, blurs, etc. ShareX is very powerful if you spend some time navigating its menus, and can become an amazing asset to your workflow... maybe I'll do another blog post in the future about some of the tips and tricks with ShareX I've discovered over the years.

Cloudflare Workers

Technically, everything should already be working at this point and you shouldn't need to do anything else if you want a basic image setup. For some extra cool-ness though, we can use Cloudflare Workers to rewrite the upload URLs to something a little more friendly, and strip the unnecessary /file/<bucket-name>/ from the URL. For example, https://subdomain.domain.com/file/<bucket-name>/test.txt would become https://subdomain.domain.com/test.txt.

Cloudflare Workers allow you to run JavaScript code on the edge of Cloudflare's network, enabling developers to build serverless applications that scale. For the worker we want to deploy, our goals are as follows:

  • Remove the /file/<bucket-name> part of the URL
  • Remove some unnecessary headers returned by Backblaze B2 assets
  • Add basic CORS headers to allow embedding of images on external sites
  • Improve caching (both browser, and edge-cache) for images

From your domain in Cloudflare, create a new worker script.

The following code I use in my worker. Simple tweak the domain and bucket variables at the top of the file with the domain this is hosted on, and the bucket name. The code below is pretty well commented, but if you have any questions, or suggestions for improvement, please do let me know.

'use strict';
const b2Domain = 'files.jross.me'; // configure this as per instructions above
const b2Bucket = 'jross-files'; // configure this as per instructions above

const b2UrlPath = `/file/${b2Bucket}/`;
addEventListener('fetch', event => {
	return event.respondWith(fileReq(event));
});

// define the file extensions we wish to add basic access control headers to
const corsFileTypes = ['png', 'jpg', 'gif', 'jpeg', 'webp'];

// backblaze returns some additional headers that are useful for debugging, but unnecessary in production. We can remove these to save some size
const removeHeaders = [
	'x-bz-content-sha1',
	'x-bz-file-id',
	'x-bz-file-name',
	'x-bz-info-src_last_modified_millis',
	'X-Bz-Upload-Timestamp',
	'Expires'
];
const expiration = 31536000; // override browser cache for images - 1 year

// define a function we can re-use to fix headers
const fixHeaders = function(url, headers){
	let newHdrs = new Headers(headers);
	// add basic cors headers for images
	if(corsFileTypes.includes(url.pathname.split('.').pop())){
		newHdrs.set('Access-Control-Allow-Origin', '*');
	}
	// override browser cache for files
	newHdrs.set('Cache-Control', "public, max-age=" + expiration);
	// set ETag for efficient caching where possible
	const ETag = newHdrs.get('x-bz-content-sha1') || newHdrs.get('x-bz-info-src_last_modified_millis') || newHdrs.get('x-bz-file-id');
	if(ETag){
		newHdrs.set('ETag', ETag);
	}
	// remove unnecessary headers
	removeHeaders.forEach((header) => {
		newHdrs.delete(header);
	});
	return newHdrs;
};
async function fileReq(event){
	const cache = caches.default; // Cloudflare edge caching
	const url = new URL(event.request.url);
	if(url.host === b2Domain && !url.pathname.startsWith(b2UrlPath)){
		url.pathname = b2UrlPath + url.pathname;
	}
	let response = await cache.match(url); // try to find match for this request in the edge cache
	if(response){
		// use cache found on Cloudflare edge. Set X-Worker-Cache header for helpful debug
		let newHdrs = fixHeaders(url, response.headers);
		newHdrs.set('X-Worker-Cache', "true");
		return new Response(response.body, {
			status: response.status,
			statusText: response.statusText,
			headers: newHdrs
		});
	}
	// no cache, fetch image, apply Cloudflare lossless compression
	response = await fetch(url, {cf: {polish: "lossless"}});
	let newHdrs = fixHeaders(url, response.headers);
	response = new Response(response.body, {
		status: response.status,
		statusText: response.statusText,
		headers: newHdrs
	});

	event.waitUntil(cache.put(url, response.clone()));
	return response;
}

Using this worker, you can now strip the /file/<bucket-name>/ section of the URL, to produce URLs like https://subdomain.domain.com/test.txt, rather than https://subdomain.domain.com/file/<bucket-name>/test.txt. Once you've deployed this worker to your subdomain subdomain.domain.com/*, test that the new URLs are working, and once you're happy, simply update the B2 destination settings in ShareX to use your new custom URL format.

ShareX Backblaze B2 Custom URL format
ShareX Backblaze B2 Custom URL format

Disclaimer

Everything I've mentioned in the post is 100% free, assuming you stay within reasonable limits.

Backblaze has a 10GB free file limit, and then charges $0.005/GB/Month thereafter. It also offers a limited number of Class "B" and "C" transactions, with 2500 free of these per day. You probably won't use any Class C transactions, but Class B transactions are used for b2_download_file_by_name requests, which is why I'd recommend caching the responses on Cloudflare for as long as you can, to prevent unnecessary B2 API requests. For more information on their transaction pricing, see their website.

Cloudflare offers a free plan to all users that will almost certainly provide what you need for the DNS and CDN aspects of this tutorial.

Cloudflare Workers also offers a free tier which includes 100,00 requests every 24 hours, with a maximum of 1,000 requests every 10 minutes. If you're just sharing these images to friends and colleagues, you'll probably be fine. See Billing for Cloudflare Workers for more information.

If you have any questions, feel free to reach out to me on Twitter.