Cloudflare + Backblaze + PicList: Building a Blog Image Host (No Domain Needed)
Previously I used a GitHub repo as an image host with PicGo, but GitHub has file-size limits and it’s a bit of abuse. Recently I finished a new setup that runs smoothly. Here’s the process.
This guide explains how to set up a free and private blog image host using Backblaze B2, Cloudflare Workers, and PicList—all without needing a domain name. It automates uploads, rewrites URLs, caches content, and protects your usage quota.
[PicList] --> (Uploads image) --> [Backblaze B2 Private Bucket]
↑
|
[Cloudflare Worker] <-- Fetch & Cache <-- [Client Request]
Why Backblaze B2?
Among object-storage providers, Backblaze is the only one offering a free-tier without requiring payment info. By default, you must use a private bucket, but you can pay $1 to make it public. I prefer to keep payments off until I fully understand the workflow.
Other reference tutorials include:
- Deliver Private Backblaze B2 Content Through Cloudflare CDN
- Cloudflare + Backblaze 实现免费的博客图床方案 - Leo’s blog
- Blog 图床方案:Backblaze B2 (私密桶) + Cloudflare Workers + PicGo | Standat’s Blog
I don’t own a domain, so I adapted most steps for my needs. This post is more of a “walk-through” than a strict tutorial—use as reference.
1. Backblaze Setup
Free Plan Overview
Backblaze B2 gives you 10 GB storage, 1 GB download/day, 2,500 class‑B transactions, and 2,500 class‑C transactions—all free. That’s plenty for blog image hosting.
Create a Private Bucket
Sign up on Backblaze, create a private bucket with a complex name.
In Bucket Settings → Bucket Info, add: {"cache-control": "public, max-age=86400"}
.This instructs caching for one day (86400 seconds).
Note the bucket endpoint—you’ll need it later.
Create Application Keys
Generate two Backblaze application keys:
- One with Read & Write access (for uploading via PicList)
- One with Read Only access (for reading via Cloudflare Workers)
Copy the keyID, keyName, and applicationKey—they appear only once.
2. Cloudflare Setup
Deploy a Cloudflare Worker
Use this Worker code to proxy and serve images via Cloudflare:
export default {
async fetch(request, env) {
const API_KEY_ID = env.API_KEY_ID; // Application Key ID
const API_KEY = env.API_KEY; // Application Key
const BUCKET_NAME = env.BUCKET_NAME; // Bucket Name
const KV_NAMESPACE = env.KV_NAMESPACE; // KV namespace for rate limiting
const ALLOWED_REFERERS = ["https://site1/",
"https://site2/",
"https://site3/",
"https://localhost:1313/"
]; // List of allowed referers
const MAX_REQUESTS_PER_DAY = 1000; // Limit to 1000 requests per day
const url = new URL(request.url);
const filePath = url.pathname.slice(1); // Extract file path from URL
if (!filePath) {
return new Response("File path not provided", { status: 400 });
}
// Step 1: Validate Referer Header
const referer = request.headers.get("Referer");
const isAllowedReferer =
!referer || ALLOWED_REFERERS.some((allowed) => referer.includes(allowed));
if (!isAllowedReferer) {
return new Response("Invalid referer", { status: 403 });
}
// Access Cloudflare Cache API
const cache = caches.default;
// Check if the response is already in cache
const cachedResponse = await cache.match(request);
if (cachedResponse) {
console.log(`Cache hit for: ${filePath}`);
return cachedResponse; // Serve from cache
}
// Step 2: Rate Limiting using KV
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const requestCountKey = `${currentDate}:${filePath}`;
let requestCount = (await KV_NAMESPACE.get(requestCountKey)) || 0;
if (requestCount >= MAX_REQUESTS_PER_DAY) {
return new Response("Daily limit reached for this file", { status: 429 });
}
requestCount = parseInt(requestCount) + 1;
await KV_NAMESPACE.put(requestCountKey, requestCount, { expirationTtl: 86400 }); // Reset every 24 hours
console.log(`Request count for ${filePath}: ${requestCount}`);
console.log(`Cache miss for: ${filePath}. Fetching from Backblaze.`);
// Step 3: Authenticate with Backblaze
const authResponse = await fetch("https://api.backblazeb2.com/b2api/v2/b2_authorize_account", {
headers: {
Authorization: `Basic ${btoa(`${API_KEY_ID}:${API_KEY}`)}`,
},
});
const authResponseText = await authResponse.text();
if (!authResponse.ok) {
return new Response(`Authorization failed: ${authResponseText}`, { status: 500 });
}
const authData = JSON.parse(authResponseText);
const authToken = authData.authorizationToken;
const downloadUrl = authData.downloadUrl; // Base URL for the bucket
// Step 4: Construct the file URL
const b2FileUrl = `${downloadUrl}/file/${BUCKET_NAME}/${filePath}`;
// Step 5: Fetch the file from Backblaze
const fileResponse = await fetch(b2FileUrl, {
headers: {
Authorization: authToken,
},
});
if (!fileResponse.ok) {
const errorText = await fileResponse.text();
return new Response(`Failed to fetch file from Backblaze: ${errorText}`, { status: 404 });
}
// Clone the response to store in cache
const responseToCache = new Response(fileResponse.body, {
headers: {
"Content-Type": fileResponse.headers.get("Content-Type"),
"Cache-Control": "public, max-age=31536000", // Cache for 1 year
},
});
// Store the response in cache
await cache.put(request, responseToCache.clone());
// Return the fetched response to the client
return responseToCache;
},
};
Key features:
- Reads
filePath
from request URL - Validates Referer header against allowed list
- Caches responses in Cloudflare edge cache
- Applies rate‑limiting using KV namespace
- Authenticates and fetches files from Backblaze
- Sets cache-control for 1‑year CDN cache once fetched
Replace placeholders with your Backblaze bucket info, application‑key details, KV namespace, and referer list. Configure rate limits (default 1 000 requests/day).
Set Secrets in Cloudflare Worker
Inside the Cloudflare dashboard, after you’ve created your Worker:
- Go to your Worker’s settings.
- Open the “Settings” → “Variables” → “Environment Variables” section (or go to “Secrets” if you’re using Wrangler CLI).
- Create the following secrets:
Name | Description |
---|---|
API_KEY_ID | The Key ID of your Backblaze read-only key. |
API_KEY | The actual Application Key for that same key. |
BUCKET_NAME | The name of your Backblaze B2 bucket. |
Create Cloudflare KV Namespace
The Worker above uses a KV namespace to store how many times each image has been accessed. If the same file is requested more than a set limit (like 1,000 times per day), the Worker will stop serving it temporarily. This prevents abuse and protects your free Backblaze quota without needing a login system.
Create a KV Namespace
- Go to your Cloudflare dashboard.
- Navigate to Workers > KV > Create a Namespace.
- Give your namespace a name (e.g.,
daily-usage-tracker
).
Bind the Namespace to Your Worker
- After creating the namespace, you’ll need to bind it to your Worker.
- Go to your Worker script in the Cloudflare dashboard.
- Under the Settings tab, find the KV Namespace Bindings section.
- Add a binding by giving it a name (e.g.,
KV_NAMESPACE
) and selecting the namespace you created earlier.
Use the Namespace in Your Script
- The binding (
KV_NAMESPACE
) will be available in theenv
object passed to your Worker script.
Summary
So the complete setup flow should include:
- Backblaze B2: Create private bucket + read-only key.
- Cloudflare Worker: Deploy image-proxying script.
- Secrets: Inject credentials (
API_KEY_ID
,API_KEY
,BUCKET_NAME
) into the Worker environment. - KV Namespace: Use Cloudflare KV to implement per-file rate-limiting.
3. PicList Setup
What is PicList?
PicList is a PicGo plugin that acts as a GUI front-end for uploading images to various platforms—including S3-compatible services like Backblaze.
Think of it as the bridge between your local clipboard and your custom image hosting backend.
Key Points in the PicList Setup
- Configure B2 Access:
- Use your key ID and application key (read & write) as AccessKeyID and SecretAccessKey for authentication
- Set
Self Endpoint
to Backblaze’s S3-compatible endpoint, e.g.https://{endpoint}
- Set ACL: private
- Bucket: Your private bucket name
- Set Cloudflare Worker url as
Self Custom URL
, this rewriteshttps://f000.backblazeb2.com/file/bucket-name/path/to/image.jpg
intohttps://your-cloudflare-subdomain.workers.dev/path/to/image.jpg
.
- Seamless Copy + Paste Experience:
- Once configured, you can just paste images into PicList, and it will:
- Upload to B2
- Return the proxied Cloudflare Worker URL
- Copy it directly to your clipboard (e.g., Markdown image tag)
- Once configured, you can just paste images into PicList, and it will:
This completes the loop: you never see or touch the raw B2 URL, and your images are always routed through your cacheable, rate-limited, domain-free, proxy-controlled CDN.
Note: PicList must use the read/write application key, while the Cloudflare Worker should only use a read-only key for security.