Table of contents
This guide will show you how to build a free hosted Cloudflare Worker that:
You might know of Cloudflare as a DNS provider, or as a CDN, or as a DDoS protection service. But Cloudflare is also a platform for building modern web applications. We run a lot of Replicate's own infrastructure on Cloudflare, and we're big fans.
Cloudflare has dozens of products for building web applications, but in this guide we'll be using just a few of them:
You know those websites that generate placeholder images for web design prototypes? You give it a width and height and it generates a placeholder image of that size.
We're going to build one of those, but with a bit more pizzaz:
We'll call it "Placeholder Zoo".
To build this app, we'll create a Cloudflare Worker that does the following:
https://example.com/800x600/sunglasses-sloth
.Here's what you'll need to build this project:
The Cloudflare team maintains an official CLI tool called create-cloudflare (also known as C3) that helps you set up and deploy new applications to Cloudflare. It's an npm package that you can run directly from the command line.
Run the following command to get started:
npm create cloudflare@latest placeholder-zoo -- \
--type=hello-world \
--lang=ts \
--deploy \
--git
This commands takes care of a lot of things for you:
.gitignore
file to avoid committing secrets to your repository.This will also automatically open a browser window to your new worker's URL:
Your new worker is now deployed to Cloudflare, but you can also run it locally.
Run the worker on your local machine to make sure everything is set up correctly:
cd placeholder-zoo
npm run dev
You should see output like this:
$ npm run dev
> placeholder-zoo@1.0.0 dev
> wrangler dev
⛅️ wrangler 3.88.0
-------------------
⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:8787
╭───────────────────────────╮
│ [b] open a browser │
│ [d] open devtools │
│ [l] turn off local mode │
│ [c] clear console │
│ [x] to exit │
╰───────────────────────────╯
Hit b to open the worker in your browser. You should see the "Hello, world!" message.
Now that you've got your worker running locally, it's time to add some secrets so your app can make authenticated requests to Replicate and Cloudflare.
Start by creating a file called .dev.vars
. Cloudflare uses this file to store secrets locally for your worker.
touch .dev.vars
Then add the following placeholder values to the .dev.vars
file:
REPLICATE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_IMAGE_ACCOUNT_HASH=
The values that go in this file are secrets, so you should treat them like passwords. Luckily, the npm create cloudflare
command you ran created a .gitignore
file that ignores the .dev.vars
file, so you don't have to worry about accidentally committing it to your repository.
Replicate API token
You'll need a Replicate API token so you can start running models from your worker.
Go to replicate.com/account/api-tokens and create a new API token, then copy it to your clipboard.
Then paste your Replicate API token into the .dev.vars
file for local development:
REPLICATE_API_TOKEN=r8_...
You'll also need to set the REPLICATE_API_TOKEN
secret in your remote worker's configuration.
Start by logging into Cloudflare using the wrangler CLI:
npx wrangler login
Note: You'll use npx wrangler
to run wrangler everywhere in this guide. Using npx ensures you're using the version of wrangler that is installed locally in the project, rather than a globally isntalled npm package, which can vary from one machine to another.
Then set your Replicate token as a secret on your deployed worker:
npx wrangler secret put REPLICATE_API_TOKEN
You should see output like this:
✔ Enter a secret value: … ****************************************
🌀 Creating the secret for the Worker "placeholder-zoo"
✨ Success! Uploaded secret REPLICATE_API_TOKEN
Cloudflare account ID
To find your Cloudflare account ID, run this command in the terminal:
npx wrangler whoami
Set the CLOUDFLARE_ACCOUNT_ID
secret in your .dev.vars
file for local development:
CLOUDFLARE_ACCOUNT_ID=...
You'll also need to set the CLOUDFLARE_ACCOUNT_ID
secret in your remote worker's configuration:
npx wrangler secret put CLOUDFLARE_ACCOUNT_ID
Cloudflare API token
To create a Cloudflare API token for Cloudflare Images, do the following:
Set the CLOUDFLARE_API_TOKEN
secret in your .dev.vars
file for local development:
CLOUDFLARE_API_TOKEN=...
You'll also need to set the CLOUDFLARE_API_TOKEN
secret in your remote worker's configuration:
npx wrangler secret put CLOUDFLARE_API_TOKEN
Cloudflare Images account hash
You'll need your Cloudflare Images account hash so you can construct URLs to your generated images.
To find your Cloudflare Images account hash:
Set the CLOUDFLARE_IMAGE_ACCOUNT_HASH
secret in your .dev.vars
file for local development:
CLOUDFLARE_IMAGE_ACCOUNT_HASH=...
You'll also need to set the CLOUDFLARE_IMAGE_ACCOUNT_HASH
secret in your remote worker's configuration:
npx wrangler secret put CLOUDFLARE_IMAGE_ACCOUNT_HASH
Now that you've added all the secrets, run this command to check that you've set them up correctly on your deployed worker:
npx wrangler secret list
You should see output like this:
[
{
"name": "CLOUDFLARE_ACCOUNT_ID",
"type": "secret_text"
},
{
"name": "CLOUDFLARE_API_TOKEN",
"type": "secret_text"
},
{
"name": "CLOUDFLARE_IMAGE_ACCOUNT_HASH",
"type": "secret_text"
},
{
"name": "REPLICATE_API_TOKEN",
"type": "secret_text"
}
]
Now that your worker is running locally and your secrets are set up, you can start running models on Replicate from your worker code.
Install the replicate
npm package:
npm install replicate
Create a new file called src/image-generator.ts
:
touch src/image-generator.ts
Paste the following code into the file:
import Replicate from 'replicate'
interface Env {
REPLICATE_API_TOKEN: string
}
export async function generateImage(prompt: string, env: Env) {
const replicate = new Replicate({auth: env.REPLICATE_API_TOKEN})
const model = 'black-forest-labs/flux-schnell'
const output = await replicate.run(model, {
input: {
prompt,
image_format: 'webp',
}
})
// Some image models return an array of output files, others just a single file.
const imageUrl = Array.isArray(output) ? output[0].url() : output.url()
console.log({imageUrl})
return imageUrl
}
Then create a new file called src/homepage.ts
:
touch src/homepage.ts
Then paste the following code into the src/homepage.ts
file:
export function homepage(): Response {
const html = `
<html>
<body>
<h1>Placeholder Zoo</h1>
<p>Examples:</p>
<ul>
<li><a href="/800x600/sunglasses-sloth">/800x600/sunglasses-sloth</a></li>
<li><a href="/512x512/psychic-goat">/512x512/psychic-goat</a></li>
<li><a href="/1024x768/hippie-lion">/1024x768/hippie-lion</a></li>
<li><a href="/600x800/punk-giraffe">/600x800/punk-giraffe</a></li>
</ul>
</body>
</html>
`;
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html' }
});
}
Replace the contents of your src/index.ts
file with the following code:
import { homepage } from './homepage'
import { generateImage } from './image-generator'
export interface Env {
REPLICATE_API_TOKEN: string
CLOUDFLARE_ACCOUNT_ID: string
CLOUDFLARE_API_TOKEN: string
CLOUDFLARE_IMAGE_ACCOUNT_HASH: string
IMAGE_CACHE: KVNamespace
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
// Example: /800x600/sunglasses-sloth
const [dimensions, animal] = url.pathname.split('/').filter(Boolean)
// Render the homepage if the path is invalid
if (!dimensions || !animal) return homepage()
// Turn `/800x600/sunglasses-sloth` into a text prompt
const [targetWidth, targetHeight] = dimensions.toLowerCase().split('x').map(n => Number.parseInt(n, 10))
const prompt = `A high-quality image of a ${animal} holding up a sign with the words "${targetWidth} by ${targetHeight}"`
// Generate the image
const imageUrl = await generateImage(prompt, env)
// Fetch the image and return it
const imageResponse = await fetch(imageUrl)
return new Response(imageResponse.body, {
headers: {
'content-type': 'image/webp',
}
})
}
}
Now run the worker again:
npm run dev
Open this URL in your browser: localhost:8787/1600x900/cinephile-rat
You should see a generated image that looks something like this:
This is another good time to commit your changes to Git:
git add .
git commit -m "Run Replicate models from the worker"
When you run models with Replicate's API, any output files generated by the model are returned as HTTPS URLs that are automatically deleted after an hour. If you want to keep your output files for longer than an hour, you need to save a copy of them somewhere.
You'll use Cloudflare Images to store your generated images, and Cloudflare KV as a key-value datastore so you can do quick lookups of images that have already been generated.
Create a new file called src/image-uploader.ts
:
touch src/image-uploader.ts
Then add the following code to the file:
interface CloudflareEnv {
CLOUDFLARE_ACCOUNT_ID: string
CLOUDFLARE_API_TOKEN: string
}
interface UploadResponse {
result: {
id: string
variants: string[]
}
}
export async function uploadToCloudflareImages (imageUrl: string, env: CloudflareEnv): Promise<string> {
console.log('Uploading image to Cloudflare Images:', imageUrl)
const imageResponse = await fetch(imageUrl)
const imageBlob = await imageResponse.blob()
const formData = new FormData()
formData.append('file', imageBlob)
const uploadResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/images/v1`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.CLOUDFLARE_API_TOKEN}`
},
body: formData
}
)
const result = (await uploadResponse.json()) as UploadResponse
if (!uploadResponse.ok) {
console.error('Failed to upload to Cloudflare Images:', result)
throw new Error('Failed to upload image')
}
console.log('Successfully uploaded to Cloudflare Images:', result)
return result.result.id
}
Then add this line to your src/index.ts
file, after the generateImage
function:
// Upload the image to Cloudflare Images
const cloudflareImageId = await uploadToCloudflareImages(imageUrl, env)
Each time you upload an image to Cloudflare Images, you'll also store some metadata about the image in Cloudflare KV. This will let you quickly look up whether an image has already been generated, so you can return the existing image without re-running the model.
Use Wrangler to create a new KV namespace:
npx wrangler kv:namespace create "IMAGE_CACHE"
You should see output like this:
⛅️ wrangler 3.88.0
-------------------
🌀 Creating namespace with title "placeholder-zoo-IMAGE_CACHE"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
[[kv_namespaces]]
binding = "IMAGE_CACHE"
id = "3c8ce31144a0467080700b241fb6bfdc"
The "configuration file" the above message is referring to is your wrangler.toml
file.
Next, update your worker code to check the KV store before generating an image, and cache the generated image ID in KV if it's not already cached.
You'll store the request pathname (e.g. /800x600/sunglasses-sloth
) as a key, and the Cloudflare Images ID (e.g. ab6b3a38-1957-4b8d-91de-3aadf0f22211
) as the value.
Overwrite the contents of your src/index.ts
file with the following code:
import { homepage } from './homepage'
import { uploadToCloudflareImages } from './image-uploader'
import { generateImage } from './image-generator'
export interface Env {
REPLICATE_API_TOKEN: string
CLOUDFLARE_ACCOUNT_ID: string
CLOUDFLARE_API_TOKEN: string
CLOUDFLARE_IMAGE_ACCOUNT_HASH: string
IMAGE_CACHE: KVNamespace
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
// Example: /800x600/sunglasses-sloth
const [dimensions, animal] = url.pathname.split('/').filter(Boolean)
// Render the homepage if the path is invalid
if (!dimensions || !animal) return homepage()
// Turn `/800x600/sunglasses-sloth` into a text prompt
const [targetWidth, targetHeight] = dimensions.toLowerCase().split('x').map(n => Number.parseInt(n, 10))
const prompt = `A high-quality image of a ${animal} holding up a sign with the words "${targetWidth} by ${targetHeight}"`
// Check for a cached image id that matches this request URL
const cacheKey = url.pathname
// If the request has a `?redo` query param, we'll bypass the cache
const shouldBypassCache = url.searchParams.has('redo')
const cachedImageId = shouldBypassCache ? null : await env.IMAGE_CACHE.get(cacheKey)
let cloudflareImageId: string
if (cachedImageId) {
console.log('Cache hit for:', cacheKey)
cloudflareImageId = cachedImageId
} else {
console.log(shouldBypassCache ? 'Bypassing cache due to redo parameter' : 'Cache miss for:', cacheKey)
// Generate the image
const replicateImageUrl = await generateImage(prompt, env)
console.log('Generated image URL:', replicateImageUrl)
// Upload the image to Cloudflare Images
cloudflareImageId = await uploadToCloudflareImages(replicateImageUrl, env)
console.log('Cloudflare Images ID:', cloudflareImageId)
// Cache the image ID
await env.IMAGE_CACHE.put(cacheKey, cloudflareImageId)
console.log('Stored in cache:', cacheKey, cloudflareImageId)
}
const transformations = {
width: targetWidth,
height: targetHeight,
fit: "cover"
}
const transformationsString = Object.entries(transformations).map(([k,v]) => `${k}=${v}`).join(',')
const transformedImageUrl = `https://imagedelivery.net/${env.CLOUDFLARE_IMAGE_ACCOUNT_HASH}/${cloudflareImageId}/${transformationsString}`;
console.log({transformedImageUrl})
// Fetch the image and return it
const imageResponse = await fetch(transformedImageUrl)
return new Response(imageResponse.body, {
headers: {
'content-type': 'image/webp',
}
})
}
}
You've been iterating on your worker locally, so now it's time to deploy your changes to Cloudflare:
npm run deploy
You should see output like this:
Deployed placeholder-zoo triggers (0.32 sec)
https://placeholder-zoo.ziki.workers.dev
Go to the URL in the output and you should see a landing page with links to example images.
If you encounter any errors when running your remote worker, you can use the wrangler tail
command to see the logs:
npx wrangler tail
Congratulations! You've built a Cloudflare Worker that:
Here are some suggestions for next steps:
Happy hacking!