| name | ffmpeg-cloudflare-containers |
| description | Complete Cloudflare Container FFmpeg system. PROACTIVELY activate for: (1) Cloudflare Containers setup, (2) Native FFmpeg at edge, (3) GPU-accelerated containers, (4) Durable Objects integration, (5) R2 storage for video files, (6) Container autoscaling, (7) Streaming large files, (8) Workers + Containers architecture, (9) Live streaming relay at edge, (10) Container vs Workers comparison. Provides: Dockerfile examples, Worker code, container configuration, GPU setup, R2 integration, production patterns. Ensures: Native FFmpeg performance at Cloudflare edge with full GPU support. |
Quick Reference
| Component | Configuration |
|---|
| Container Class | export class FFmpegContainer extends Container |
| Dockerfile | FROM jrottenberg/ffmpeg:7.1-alpine |
| wrangler.toml | [[containers]] with class_name and image |
| Feature | Workers (ffmpeg.wasm) | Containers (Native) |
|---|
| Size Limit | 10MB | Unlimited |
| Performance | 10-100x slower | Native speed |
| GPU Support | None | NVIDIA available |
| Cold Start | Instant | 2-3 seconds |
When to Use This Skill
Use for edge video processing at scale:
- Native FFmpeg performance at Cloudflare edge
- GPU-accelerated transcoding in containers
- Large file processing (>10MB)
- Complex FFmpeg pipelines
- R2 storage integration for video files
FFmpeg in Cloudflare Containers (2025)
Overview
Cloudflare Containers (launched June 2025) enable running FFmpeg natively in a full Linux container environment at the edge. This overcomes all limitations of Workers-based approaches (ffmpeg.wasm size limits, SharedArrayBuffer restrictions).
Key Benefits
| Feature | Workers (ffmpeg.wasm) | Containers (Native) |
|---|
| Binary Size | Limited to 10MB | Unlimited (disk space) |
| Performance | WebAssembly overhead | Native speed |
| GPU Support | None | NVIDIA GPUs available |
| Filesystem | Virtual/memory only | Full Linux FS |
| Dependencies | Must compile to WASM | Any Linux package |
| Cold Start | Instant | 2-3 seconds |
Getting Started
Prerequisites
docker info
npx wrangler --version
Create Container Project
npm create cloudflare@latest -- --template=cloudflare/templates/containers-template
wrangler.toml Configuration
name = "ffmpeg-processor"
main = "src/index.ts"
compatibility_date = "2025-12-19"
[[containers]]
class_name = "FFmpegContainer"
image = "./Dockerfile"
max_instances = 10
[[durable_objects.bindings]]
name = "FFMPEG_CONTAINER"
class_name = "FFmpegContainer"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["FFmpegContainer"]
Dockerfile for FFmpeg
# Use official FFmpeg image
FROM jrottenberg/ffmpeg:7.1-alpine AS ffmpeg
# Production image
FROM node:22-alpine
# Install FFmpeg from the official image
COPY --from=ffmpeg /usr/local /usr/local
# Install additional dependencies
RUN apk add --no-cache \
libva \
libva-utils \
mesa-va-gallium
# Set up application
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Container must listen on a port
EXPOSE 8080
CMD ["node", "server.js"]
Worker Code
import { Container, DurableObject } from "cloudflare:workers";
interface Env {
FFMPEG_CONTAINER: DurableObjectNamespace<FFmpegContainer>;
}
export class FFmpegContainer extends Container {
defaultPort = 8080;
sleepAfter = "5m";
envVars = {
NODE_ENV: "production",
FFMPEG_PATH: "/usr/local/bin/ffmpeg"
};
async onStart() {
console.log("FFmpeg container started");
}
async onStop() {
console.log("FFmpeg container stopped");
}
async onError(error: Error) {
console.error("Container error:", error);
}
}
export default {
async fetch(: , : ): <> {
url = (request.);
(url..()) {
id = env..(crypto.());
container = env..(id);
container.(request);
}
(url. === ) {
id = env..();
container = env..(id);
container.(request);
}
(, { : });
}
};
Container Server (Node.js)
import express from 'express';
import { spawn } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
const app = express();
app.use(express.raw({ type: '*/*', limit: '100mb' }));
const TEMP_DIR = '/tmp/ffmpeg-work';
app.post('/transcode', async (req, res) => {
const jobId = Date.now().toString();
const workDir = path.join(TEMP_DIR, jobId);
await fs.mkdir(workDir, { recursive: true });
try {
const inputPath = path.join(workDir, 'input');
await fs.writeFile(inputPath, req.body);
const format = req.query.format || ;
outputPath = path.(workDir, );
result = ([
, inputPath,
, ,
, ,
, ,
, ,
, ,
, ,
outputPath
]);
(result. !== ) {
(result.);
}
output = fs.(outputPath);
res.(, );
res.(output);
} {
fs.(workDir, { : , : });
}
});
app.(, (req, res) => {
jobId = .().();
workDir = path.(, jobId);
fs.(workDir, { : });
{
inputPath = path.(workDir, );
palettePath = path.(workDir, );
outputPath = path.(workDir, );
fs.(inputPath, req.);
([
, inputPath,
, ,
, palettePath
]);
([
, inputPath,
, palettePath,
, ,
, outputPath
]);
output = fs.(outputPath);
res.(, );
res.(output);
} {
fs.(workDir, { : , : });
}
});
app.(, {
res.({ : , : process.. });
});
() {
( {
proc = (, args, { : [, , ] });
stdout = ;
stderr = ;
proc..(, { stdout += data; });
proc..(, { stderr += data; });
proc.(, {
({ exitCode, stdout, stderr });
});
});
}
= process.. || ;
app.(, {
.();
});
GPU-Accelerated Containers
Cloudflare Containers support NVIDIA GPUs for hardware-accelerated encoding:
GPU Container Configuration
[[containers]]
class_name = "FFmpegGPU"
image = "./Dockerfile.gpu"
max_instances = 5
GPU Dockerfile
FROM nvidia/cuda:12.4-runtime-ubuntu24.04
# Install FFmpeg with NVIDIA support
RUN apt-get update && apt-get install -y \
ffmpeg \
libnvidia-encode-550 \
libnvidia-decode-550 \
&& rm -rf /var/lib/apt/lists/*
# Verify NVENC support
RUN ffmpeg -encoders | grep nvenc
WORKDIR /app
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
NVENC Encoding in Container
app.post('/transcode-gpu', async (req, res) => {
const args = [
'-hwaccel', 'cuda',
'-hwaccel_output_format', 'cuda',
'-i', inputPath,
'-c:v', 'h264_nvenc',
'-preset', 'p4',
'-cq', '23',
'-c:a', 'aac',
'-b:a', '128k',
outputPath
];
await runFFmpeg(args);
});
Autoscaling Configuration
export class FFmpegContainer extends Container {
static scaling = {
minInstances: 1,
maxInstances: 20,
targetCPU: 75,
scaleDownDelay: "5m"
};
sleepAfter = "10m";
}
Pricing Considerations
| Metric | Billing |
|---|
| Compute | Per 10ms while active |
| Idle | No charge (containers sleep) |
| Storage | Per GB-month |
| Egress | Standard Cloudflare egress |
Best Practices
1. Container Size Optimization
# Use multi-stage builds
FROM jrottenberg/ffmpeg:7.1-alpine AS ffmpeg
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Final minimal image
FROM node:22-alpine
COPY --from=ffmpeg /usr/local/bin/ffmpeg /usr/local/bin/
COPY --from=ffmpeg /usr/local/bin/ffprobe /usr/local/bin/
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node", "server.js"]
2. Warm Instance Strategy
export class FFmpegContainer extends Container {
sleepAfter = "30m";
}
export default {
async scheduled(controller: ScheduledController, env: Env) {
const id = env.FFMPEG_CONTAINER.idFromName("warmup");
const container = env.FFMPEG_CONTAINER.get(id);
await container.fetch(new Request("http://internal/health"));
}
};
3. Streaming Large Files
app.post('/transcode-stream', async (req, res) => {
const ffmpeg = spawn('ffmpeg', [
'-i', 'pipe:0',
'-c:v', 'libx264',
'-f', 'mp4',
'-movflags', 'frag_keyframe+empty_moov',
'pipe:1'
]);
req.pipe(ffmpeg.stdin);
ffmpeg.stdout.pipe(res);
ffmpeg.stderr.on('data', (data) => {
console.log('FFmpeg:', data.toString());
});
});
4. R2 Storage Integration
export default {
async fetch(request: Request, env: Env) {
const result = await container.fetch(request);
const key = `processed/${Date.now()}.mp4`;
await env.R2_BUCKET.put(key, result.body);
return new Response(JSON.stringify({ key }), {
headers: { 'Content-Type': 'application/json' }
});
}
};
Common Use Cases
Video Thumbnails at Edge
app.post('/thumbnail', async (req, res) => {
await runFFmpeg([
'-i', inputPath,
'-ss', '00:00:01',
'-vframes', '1',
'-vf', 'scale=320:-1',
outputPath
]);
});
Audio Extraction
app.post('/extract-audio', async (req, res) => {
await runFFmpeg([
'-i', inputPath,
'-vn',
'-c:a', 'aac',
'-b:a', '192k',
outputPath
]);
});
Live Streaming Relay
const ffmpeg = spawn('ffmpeg', [
'-i', rtmpInput,
'-c:v', 'copy',
'-c:a', 'copy',
'-hls_time', '4',
'-hls_list_size', '10',
'-hls_flags', 'delete_segments',
'/output/stream.m3u8'
]);
Deployment
wrangler deploy
Monitoring
app.get('/metrics', async (req, res) => {
const metrics = {
uptime: process.uptime(),
memory: process.memoryUsage(),
deploymentId: process.env.CLOUDFLARE_DEPLOYMENT_ID
};
res.json(metrics);
});
Comparison: Workers vs Containers
| Use Case | Recommendation |
|---|
| Simple filters (resize, crop) | Workers + ffmpeg.wasm |
| Full transcoding | Containers |
| GPU acceleration | Containers (GPU instances) |
| Low latency (<100ms) | Workers |
| Large files (>10MB) | Containers |
| Complex pipelines | Containers |
Troubleshooting
Container Not Starting
docker build -t test-ffmpeg .
docker run --rm test-ffmpeg ffmpeg -version
docker inspect test-ffmpeg | grep Architecture
FFmpeg Not Found
# Ensure FFmpeg is in PATH
ENV PATH="/usr/local/bin:${PATH}"
# Verify installation
RUN which ffmpeg && ffmpeg -version
Timeout Issues
export class FFmpegContainer extends Container {
async fetch(request: Request) {
const controller = new AbortController();
setTimeout(() => controller.abort(), 300000);
return super.fetch(request, { signal: controller.signal });
}
}
References