Skip to main content

Deploying a Renderer to Cloudflare Containers

This guide provides a complete walkthrough for deploying a Remotion rendering service on Cloudflare Containers. The final rendered videos will be uploaded to a Cloudflare R2 bucket for persistent storage.

Prerequisites

  • A working Remotion project (e.g., from npm create video).
  • A Cloudflare account.
  • The Wrangler CLI installed (npm i -g wrangler) and authenticated (wrangler login).
  • An R2 bucket created in your Cloudflare dashboard.
  • Your R2 bucket's "Public access" URL enabled. In your bucket's settings, click "Allow Access" and copy the public URL (e.g., https://pub-xxxxxxxx.r2.dev).

Step 1: Add Dependencies

Your server will need express to handle HTTP requests and the @aws-sdk/client-s3 package to communicate with the S3-compatible R2 API.

bash
npm install express @aws-sdk/client-s3

Step 2: Create a Dockerfile

Create a Dockerfile in your project's root directory. This file defines the container image, installing all necessary system dependencies for Remotion's browser rendering.

Dockerfile
docker
FROM node:22-bookworm-slim
# Install Chrome dependencies
RUN apt-get update
RUN apt install -y \
libnss3 \
libdbus-1-3 \
libatk1.0-0 \
libgbm-dev \
libasound2 \
libxrandr2 \
libxkbcommon-dev \
libxfixes3 \
libxcomposite1 \
libxdamage1 \
libatk-bridge2.0-0 \
libpango-1.0-0 \
libcairo2 \
libcups2
# Copy everything from your project to the Docker image. Adjust if needed.
COPY package.json package*.json yarn.lock* pnpm-lock.yaml* bun.lockb* bun.lock* tsconfig.json* remotion.config.* ./
COPY src ./src
# If you have a public folder:
COPY public ./public
# Install the right package manager and dependencies
RUN npm i
# Install Chrome
RUN npx remotion browser ensure
# The command to start the server
CMD ["node", "server.js"]

Step 3: Create the Render Server

Create a server.js file in your root. This Express server will handle render requests, orchestrate the render with Remotion, upload the result to R2, and return the public URL.

server.js
javascript
import {renderMedia, getCompositions} from '@remotion/renderer';
import {S3Client, PutObjectCommand} from '@aws-sdk/client-s3';
import express from 'express';
import fs from 'fs/promises';
import path from 'path';
const app = express();
app.use(express.json());
const port = process.env.PORT || 8080;
// These environment variables will be set in wrangler.toml
const BUCKET_NAME = process.env.R2_BUCKET_NAME;
const BUCKET_PUBLIC_URL = process.env.R2_PUBLIC_URL;
const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
const s3 = new S3Client({
region: 'auto',
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
// Credentials are not needed when running in the Cloudflare environment
// as they are automatically provided by the R2 binding.
});
app.post('/render', async (req, res) => {
const {compositionId, inputProps} = req.body;
const outputLocation = path.join('/tmp', `out-${Date.now()}.mp4`);
try {
if (!compositionId) {
return res.status(400).send({message: '`compositionId` is required.'});
}
if (!BUCKET_NAME || !ACCOUNT_ID || !BUCKET_PUBLIC_URL) {
throw new Error('Server is missing R2 environment variables.');
}
const compositions = await getCompositions({entryPoint: './src/index.ts'});
const composition = compositions.find((c) => c.id === compositionId);
if (!composition) {
return res.status(404).send({message: `Composition '${compositionId}' not found.`});
}
console.log('Starting render...');
await renderMedia({composition, inputProps, codec: 'h264', outputLocation});
console.log('Render finished.');
const fileBuffer = await fs.readFile(outputLocation);
const objectKey = `renders/${path.basename(outputLocation)}`;
console.log(`Uploading to R2 bucket: ${BUCKET_NAME}`);
await s3.send(
new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: objectKey,
Body: fileBuffer,
ContentType: 'video/mp4',
}),
);
console.log('Upload complete.');
const publicUrl = `${BUCKET_PUBLIC_URL}/${objectKey}`;
res.status(200).send({outputUrl: publicUrl});
} catch (err) {
console.error(err);
res.status(500).send({message: 'Error rendering video.', error: err.message});
} finally {
// Clean up the temporary file from the container's filesystem
if (await fs.stat(outputLocation).catch(() => null)) {
await fs.unlink(outputLocation);
}
}
});
app.listen(port, () => {
console.log(`Renderer server listening on port ${port}`);
});

Step 4: Configure wrangler.toml

The wrangler.toml file configures your Cloudflare service. Create it in your root. We will define environment variables and bind our R2 bucket, which securely grants the container access.

wrangler.toml
toml
name = "remotion-renderer-service"
# Define environment variables for the container
[vars]
R2_BUCKET_NAME = "your-r2-bucket-name" # The name of your R2 bucket
R2_PUBLIC_URL = "https://pub-xxxxxxxx.r2.dev" # The public URL for your bucket
CLOUDFLARE_ACCOUNT_ID = "your-account-id" # Find this in your Cloudflare dashboard
# Add an R2 binding to grant access
[[r2_buckets]]
binding = "R2_BUCKET" # This name is conventional but not used directly in the server.js
bucket_name = "your-r2-bucket-name"

Step 5: Deploy

With all files in place, deploy the service from your terminal.

bash
wrangler deploy

Wrangler will build the Docker image, push it to Cloudflare, and deploy your service. It will then output your service's public URL.

Deployment Output (example)
bash
✅ Successfully deployed my-remotion-renderer (9.34s)
🔗 https://remotion-renderer-service.your-subdomain.workers.dev

Step 6: Trigger a Render

Send a POST request to your service's /render endpoint. Replace YOUR_SERVICE_URL with the URL from the deployment output.

bash
curl -X POST https://YOUR_SERVICE_URL/render \
-H "Content-Type: application/json" \
-d '{
"compositionId": "HelloWorld",
"inputProps": {
"titleText": "Hello from Cloudflare Containers!",
"titleColor": "#0B5394"
}
}'

Response

If successful, the API will respond with a JSON object containing the public URL of your video, hosted on R2.

Example Response
json
{
"outputUrl": "https://pub-xxxxxxxx.r2.dev/renders/out-1678886400000.mp4"
}

That's it! You can now use this endpoint to programmatically render videos on demand.

warning

The deployed endpoint is public by default. For production use, you should implement an authentication mechanism (e.g., a secret token in the request header) to secure your rendering API and prevent unauthorized use.