A modernized, simple image proxy and CDN utilizing Cloudflare Workers.
Based on a fork of cf-image-proxy
Modernized with:
- 🎨 Biome - Modern linting and formatting
- 📘 TypeScript - Type-safe development
- ⚡ ES2022 - Latest JavaScript features
- 🧪 Vitest - Fast unit testing
- 🔧 Miniflare - Local development server
- 📦 ESM - Native ES modules
Image proxy and CDN for Cloudflare Workers.
- Free 💪
- Super simple to setup and self-host
- Perfect lighthouse scores
- Handles CORS for you
- Normalizes origin URLs
- Respects
pragma: no-cacheand related headers - Used in hundreds of prod sites
- Fork or clone this repo
cp wrangler.example.toml wrangler.tomlnpx wrangler loginto authenticate with your Cloudflare accountnpx wrangler whoamito get youraccount_id- Update the missing values in wrangler.toml
npm inpm run deployto deploy to Cloudflare Workers 💪
name = "cf-image-proxy"
main = "src/index.js"
account_id = "YOUR_ACCOUNT_ID"
workers_dev = true
compatibility_date = "2025-10-30"
[[routes]]
pattern = "YOUR_DOMAIN" # e.g. img.example.com
custom_domain = true # auto creates Cloudflare DNS records for youYou can optionally enable Polish in your Cloudflare zone settings if you want to enable on-the-fly image optimization as part of your CDN. In many cases, this will serve images to supported clients in an optimized webp format.
This may increase costs, so it's not recommended for everyone. The CF worker should support both configurations without issue.
By default, all assets will be served with a cache-control header set to public, immutable, s-maxage=31536000, max-age=31536000, stale-while-revalidate=60, which effectively makes them cached at all levels indefinitely (or more practically until Cloudflare or your browser purges the asset from its cache).
If you want to change this cache-control header or add additional headers, see src/fetch-request.js.
The project uses Biome for linting and formatting. Available commands:
npm run dev- Start a local dev server with Wrangler (Miniflare)npm test- Run tests with Vitestnpm run lint- Check for linting issuesnpm run preview- Preview the worker in the Cloudflare edge runtimenpm run deploy- Builds and deploy the worker to Cloudflarenpm run build- Perform a dry-run build of the workernpm run format- Format all filesnpm run lint- Check for linting issuesnpm run typecheck- Run TypeScript type checking
In the application where you want to consume your proxied images, you'll need to replace your third-party image URLs.
You can replace them with your proxy domain plus a path that contains the URI-encoded version of your original domain. In TypeScript, this looks like the following:
const imageCDNHost = 'https://exampledomain.com'
export const mapImageUrl = (imageUrl: string) => {
if (imageUrl.startsWith('data:')) {
return imageUrl
}
if (imageCDNHost) {
// Our proxy uses Cloudflare's global CDN to cache these image assets
return `${imageCDNHost}/${encodeURIComponent(imageUrl)}`
} else {
return imageUrl
}
}A few notes about the implementation:
- It is hosted via Cloudflare (CF) edge workers.
- CF runs our worker via V8 directly in an environment mimicking web workers.
- It does have access to a custom web fetch API.
- Support restricting the origin domain in order to prevent abuse
- Add a snazzy demo
MIT © Travis Fischer
Support my OSS work by following me on twitter