Image-CDN mit Cloudflare R2 und Workers aufbauen
R2 kann Ihre Quellbilder speichern, ein Worker kann eine Variante auswählen, und das Binding von Cloudflare Images kann sie in der Größe ändern und codieren. Das sind separate Services mit separaten Nutzungslimits. Ein kleines Projekt passt möglicherweise in deren kostenlose Kontingente, aber ein Image-CDN ist nicht bedingungslos kostenlos.
Dieses Beispiel veröffentlicht eine kleine, ausdrücklich freigegebene Menge an Bildern. Es ist kein Gateway für private Dateien und kein beliebiger Upload-Prozessor.
Warum Cloudflare R2 für Ihr Image-CDN?
R2 trennt Object Storage von der Auslieferung. Halten Sie den Bucket privat und lassen Sie den Worker freigegebene Objekte über ein Binding lesen. Der Worker gibt transformierte Bytes zurück, statt eine R2-URL offenzulegen.
Eine Größenänderung erfordert einen Dienst für Bildtransformationen: cf.image auf einem Response zu platzieren,
transformiert dessen Body nicht. Wir verwenden das eigentliche
Images-Binding.
Ihr Image-CDN einrichten
Schritt 1: Cloudflare-Konto erstellen
Aktivieren Sie R2, Workers und Images in Ihrem Konto und prüfen Sie deren aktuelle
Abrechnungseinstellungen. Installieren Sie Node.js und erstellen Sie ein Worker-Projekt mit der
Cloudflare-Einstiegsanleitung.
Verwenden Sie einen ES-Module-Worker; die Implementierung unten ist src/index.js.
Schritt 2: R2-Bucket erstellen
Erstellen Sie im R2-Dashboard einen Bucket mit dem Namen image-cdn-demo. Lassen Sie sowohl die öffentliche
Entwicklungs-URL als auch die benutzerdefinierten Domains für öffentliche Buckets deaktiviert. Laden
Sie nur Bilder hoch, die Sie veröffentlichen dürfen.
Unsere Allowlist bildet den öffentlichen Dateinamen photo.jpg auf public/photo-v1.jpg ab. Prüfen Sie vor dem
Upload, dass es sich um ein JPEG oder PNG mit einem einzelnen Frame handelt, das nicht größer als
8 MiB und 12 Millionen decodierte Pixel ist. Entfernen Sie private Metadaten während Ihres
Veröffentlichungsprozesses. Überschreiben Sie niemals versionierte Objekte.
Schritt 3: Worker erstellen
Der vollständige Worker unterstützt drei Breiten und zwei Formate. Indem unbekannte oder doppelte
Parameter abgelehnt werden, bleibt die Menge der Transformationen endlich. Das Format ist explizit
in der URL angegeben, sodass der Cache nicht vom Browser-Header Accept abhängt.
const published = new Map([['photo.jpg', 'public/photo-v1.jpg']])
const widths = new Set(['320', '640', '1280'])
const formats = new Map([['webp', 'image/webp'], ['jpeg', 'image/jpeg']])
const MAX_BYTES = 8 * 1024 * 1024
function failure(status, message, extra = {}) {
return new Response(message, {
status,
headers: { 'Cache-Control': 'no-store', 'Content-Type': 'text/plain; charset=utf-8', ...extra },
})
}
export default {
async fetch(request, env, ctx) {
if (request.method !== 'GET' && request.method !== 'HEAD') {
return failure(405, 'Use GET or HEAD.', { Allow: 'GET, HEAD' })
}
const url = new URL(request.url)
const key = published.get(url.pathname.slice(1))
if (!key) return failure(404, 'Image not found.')
const pairs = [...url.searchParams]
if (pairs.some(([name]) => name !== 'w' && name !== 'f') ||
url.searchParams.getAll('w').length > 1 || url.searchParams.getAll('f').length > 1) {
return failure(400, 'Unsupported image parameters.')
}
const width = url.searchParams.get('w') ?? '640'
const format = url.searchParams.get('f') ?? 'webp'
if (!widths.has(width) || !formats.has(format)) {
return failure(400, 'Unsupported image variant.')
}
// Normalize defaults/order and include the immutable source version in the internal cache key.
const cacheUrl = new URL('/_image-cache/' + key, url.origin)
cacheUrl.searchParams.set('w', width)
cacheUrl.searchParams.set('f', format)
const cacheKey = new Request(cacheUrl, { method: 'GET' })
const cache = caches.default
try {
let response = await cache.match(cacheKey)
if (!response) {
const object = await env.IMAGES_BUCKET.get(key)
if (!object) return failure(404, 'Image not found.')
if (object.size === 0 || object.size > MAX_BYTES) {
await object.body.cancel()
return failure(422, 'Image is outside the supported limits.')
}
const output = await env.IMAGES.input(object.body)
.transform({ width: Number(width) })
.output({ format: formats.get(format) })
const transformed = output.response()
response = new Response(transformed.body, transformed)
response.headers.set('Cache-Control', 'public, max-age=3600')
response.headers.set('X-Content-Type-Options', 'nosniff')
// Public images only: this endpoint does not authorize private content.
response.headers.set('Access-Control-Allow-Origin', '*')
ctx.waitUntil(cache.put(cacheKey, response.clone()).catch(() => {
console.error('Image cache write failed.')
}))
}
return request.method === 'HEAD'
? new Response(null, response)
: response
} catch {
console.error('Image delivery failed.')
return failure(502, 'Image is temporarily unavailable.')
}
},
}
Schritt 4: Worker-Bindings konfigurieren
Fügen Sie diese Bindings zu Ihrer generierten Wrangler-Konfiguration hinzu und behalten Sie die übrigen Felder des Projekts bei:
{
"name": "image-cdn-demo",
"main": "src/index.js",
"compatibility_date": "2026-07-30",
"r2_buckets": [
{ "binding": "IMAGES_BUCKET", "bucket_name": "image-cdn-demo" }
],
"images": { "binding": "IMAGES" }
}
Diese Implementierung verwendet die Workers Cache API ausdrücklich. Fügen Sie keinen zweiten automatischen Response-Cache hinzu, ohne dessen Cache-Key, Invalidierung und Autorisierungsverhalten zu prüfen.
Schritt 5: Ihre Domain einrichten
Starten Sie lokal mit npx wrangler dev. Das lokale R2 ist vom bereitgestellten Bucket getrennt; befüllen Sie
es vor dem Testen mit einem lokalen Objekt. Die lokale Images-Emulation unterstützt nur einen Teil
der Produktionsoptionen, prüfen Sie daher Ihre bereitgestellten Varianten, bevor Sie Traffic darauf
leiten.
Stellen Sie den Worker mit npx wrangler deploy bereit. Fügen Sie in den Worker-Einstellungen eine
Workers Custom Domain
für eine Domain in Ihrem Cloudflare-Konto hinzu. Ein DNS-Eintrag allein bindet den Worker nicht an.
Bilder hochladen und verwenden
Befüllen Sie den lokalen Entwicklungs-Bucket mit Ihrer geprüften Quelldatei:
npx wrangler r2 object put image-cdn-demo/public/photo-v1.jpg --file ./photo.jpg --local
curl --fail-with-body 'http://localhost:8787/photo.jpg?w=320&f=webp' --output photo-320.webp
curl --fail-with-body --head 'http://localhost:8787/photo.jpg?f=jpeg&w=640'
Laden Sie für das Deployment dieselbe freigegebene Datei über das R2-Dashboard hoch oder verwenden
Sie bewusst das Flag --remote von Wrangler. Lokale Uploads werden nicht automatisch in die Produktion
kopiert.
Kostenüberlegungen
Prüfen Sie vor dem Deployment die aktuellen R2-Preise, Workers-Preise und Images-Preise. Storage, Objektlesevorgänge, Worker-Anfragen und Bildtransformationen haben unterschiedliche Kontingente und Abrechnungsregeln. Das Überschreiten eines kostenlosen Kontingents kann je nach Service und Tarif Kosten verursachen oder dazu führen, dass Anfragen abgelehnt werden. Leiten Sie aus der Egress-Richtlinie von R2 kein kostenloses Kontingent für Transformationen ab.
Kalkulieren Sie Cache-Misses und neue Quellversionen ein. Ein Treffer im Edge-Cache ist keine Garantie dafür, dass jede künftige Anfrage Speicher- oder Transformationsaufwand vermeidet.
Bewährte Verfahren für die Sicherheit
Die Allowlist ist eine Veröffentlichungsgrenze, kein Authentifizierungsmechanismus. Wer eine freigegebene URL kennt, kann das zugehörige Bild abrufen. Ordnen Sie ihr keine persönlichen Uploads oder Dateien mit Zugriffskontrollen zu.
Für die private Auslieferung sollten Sie Autorisierung und privates Caching gemeinsam entwerfen, bevor Sie diesen Code anpassen. Bewahren Sie Zugangsdaten in Worker-Bindings oder Secrets auf, niemals in Query-Strings. Setzen Sie Missbrauchskontrollen und Nutzungswarnungen auf Kontoebene ein. Ein Zähler mit KV-Lese-, Inkrement- und Schreibvorgängen ist kein atomarer Rate-Limiter.
CORS-Anfragen verarbeiten
Die erfolgreiche Bildantwort erlaubt jeden Ursprung, da diese Bilder öffentlich sind. Das erlaubt
die Verwendung von fetch() und Canvas im Browser ohne Credentials. Die gewöhnliche ursprungsübergreifende
Anzeige mit img erfordert für sich genommen kein CORS. Der Endpunkt unterstützt keine Anfragen mit
Credentials und keine benutzerdefinierten Request-Header, die einen Preflight erfordern.
Fehlerbehandlung
Ungültige Parameter liefern 400, nicht veröffentlichte Namen liefern 404 und nicht unterstützte
Methoden liefern 405. Fehlende oder unbrauchbare Quelldateien greifen nicht auf das Originalbild
zurück. Eine fehlgeschlagene Transformation liefert eine bereinigte 502-Antwort mit no-store zurück,
nicht Anbieterdetails oder zwischengespeicherte Fehlerseiten.
Unterstützte Bildtransformationen
Verwenden Sie w=320, w=640 oder w=1280, zusammen mit f=webp oder f=jpeg. Werden Parameter weggelassen,
wird WebP mit 640 Pixeln gewählt. Eine Größenänderung nur über die Breite erhält das
Seitenverhältnis; die Höhe ergibt sich aus der freigegebenen Quelle. JPEG erhält keine Transparenz.
Veröffentlichen Sie deckende, also nicht transparente Quellbilder, wenn beide Varianten identisch
aussehen müssen. Zusätzliche Formate oder Zuschneideregeln erfordern explizite Änderungen an der
Allowlist und an den Tests.
Einschränkungen bei Bildern
Der Worker prüft die Bytegröße des Objekts. Pixelanzahl, Einzelframe-Format und Veröffentlichungsberechtigung sind Anforderungen zum Zeitpunkt der Veröffentlichung und werden nicht dadurch durchgesetzt, dass einem Dateinamen oder einem Content-Type-Header vertraut wird. Machen Sie diesen Bucket nicht für nicht vertrauenswürdige Uploader beschreibbar. Große oder nicht unterstützte Objekte müssen vor der Veröffentlichung abgelehnt werden; der Anbieter wendet zusätzlich eigene Decoder-Limits an.
Caching-Verhalten
Eine erfolgreiche Antwort ist eine Stunde lang öffentlich. Der interne Schlüssel enthält die Version des Quellobjekts, die Breite und das Format. Die Reihenfolge der Query-Parameter und weggelassene Standardwerte führen zum selben internen Schlüssel, während JPEG und WebP niemals denselben zwischengespeicherten Body teilen.
Die Cache API ist auf das jeweilige Cloudflare-Rechenzentrum beschränkt; sie ist kein global replizierter Objektspeicher. Verwenden Sie einen neuen öffentlichen Dateinamen für Aktualisierungen, die Browser-Caches sofort umgehen müssen. Allein das zugrunde liegende Objekt der Allowlist zu ändern, kann eine bereits zwischengespeicherte Browser-Antwort nicht ungültig machen.
Umgebungsvariablen einrichten
Dieses Beispiel benötigt kein Bearer-Token, keinen KV-Namespace und kein optionales Authentifizierungs-Flag. Die Bindings stellen den privaten Lesezugriff auf R2 und die Verbindung zum Bilddienst bereit. Halten Sie Entwicklungs- und Produktions-Buckets getrennt und verwenden Sie in automatisierten Tests nicht versehentlich Remote-Bindings.
Fazit
R2, Workers und Images liefern die Bausteine für einen Bildauslieferungsdienst. Halten Sie Veröffentlichungsentscheidungen, Variantenauswahl und Caching explizit, prüfen Sie die aktuellen technischen Service-Spezifikationen und behalten Sie alle drei Nutzungsbudgets im Blick. Eine verwaltete Alternative ist das Smart CDN von Transloadit.
