FFmpeg in Docker: the Dockerfile Isn't the Hard Part

You need FFmpeg inside a container. The Dockerfile is four lines and it works on the first try. Then the image is 300 MB, the build takes 25 minutes on CI, and your first real encode gets killed with exit code 137.
Quick answer: The shortest working ffmpeg docker setup isFROM alpine:3.22plusRUN apk add --no-cache ffmpeg, which gives you a container with FFmpeg 7.1 and ffprobe on the PATH in about 100 MB on disk. Use a Debian or Ubuntu base if you need a specific FFmpeg version or non-free codecs, and use a multi-stage build only if you're compiling from source, sinceapk addalready installs runtime libraries only.
Conventional wisdom says containerizing FFmpeg solves the dependency problem. It does. What it doesn't solve is everything that made you want to containerize in the first place: the encode still needs CPU you have to pay for, disk you have to mount, a queue so a 20-minute job doesn't block your API, and a retry story for when the worker dies at minute 18. The Dockerfile is the easy 10%.
The shortest FFmpeg Dockerfile that works
Alpine's community repository ships an ffmpeg package, so the whole build is one apk add. This is the correct starting point for 90% of use cases.
FROM alpine:3.22
RUN apk add --no-cache ffmpeg
ENTRYPOINT ["ffmpeg"]
Build it and run a real conversion against a mounted directory:
docker build -t my-ffmpeg .
docker run --rm -v "$PWD:/work" -w /work my-ffmpeg \
-i input.mov -c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 128k output.mp4
The --no-cache flag matters. Without it, apk leaves its package index in /var/cache/apk, which adds a few megabytes of pure waste to the layer. The -w /work sets the working directory so relative paths in your FFmpeg command resolve against the mount, not against /.
If you want ffprobe as your entrypoint sometimes and ffmpeg other times, drop the ENTRYPOINT line and pass the full command instead. Both binaries come from the same package. For what ffprobe can tell you before you spend CPU on a transcode, see inspecting video metadata with ffprobe.
Which base image for your FFmpeg Docker build
Base image choice is really a choice about FFmpeg version and codec availability, not about size. What each option gives you:
| Base image | Base size (on disk) | FFmpeg version | Notes |
|---|---|---|---|
| `alpine:3.22` | ~8 MB | 7.1.x | musl libc, smallest total, community repo |
| `alpine:3.20` | ~8 MB | 6.1.x | pin here if you need FFmpeg 6 behavior |
| `debian:bookworm-slim` | ~75 MB | 5.1.x | older FFmpeg, glibc, best binary compatibility |
| `ubuntu:24.04` | ~78 MB | 6.1.x | glibc, good NVIDIA/CUDA driver story |
| `jrottenberg/ffmpeg:7-alpine` | prebuilt | 7.x | maintained image, wide codec set, no build step |
The Alpine tradeoff is musl libc instead of glibc. That bites you if you drop in a precompiled binary from elsewhere, or link FFmpeg into a Node or Python app with native modules built against glibc. If any part of your stack assumes glibc, take the 75 MB hit and use debian:bookworm-slim.
The Debian tradeoff is FFmpeg 5.1, which is from 2022. Filters added in FFmpeg 6 and 7 won't be there. Check ffmpeg -filters | grep yourfilter inside the container before you commit to a base.
The image-size trap
Two numbers get quoted for the same image and they differ by roughly 3x. Docker Hub displays compressed size, which is what you download. docker images displays uncompressed on-disk size, which is what fills your host. A jrottenberg/ffmpeg Alpine tag listed at 30 to 35 MB on Docker Hub lands closer to 100 MB when you actually pull it.
So "FFmpeg in Alpine is only 35 MB" is the wire number, not the disk number. Verify with:
docker images my-ffmpeg --format '{{.Repository}}:{{.Tag}} {{.Size}}'
The second half of the trap hits when apk add ffmpeg lacks the codec you need and you compile from source. A build stage with build-base, nasm, yasm, pkgconf, and the -dev packages for x264, x265, libvpx, opus, and lame adds 400 to 600 MB of toolchain. A full ./configure && make runs 20 to 40 minutes on 2 vCPUs, and it runs again on every CI cache miss. You traded a 4-line Dockerfile for a build that costs more in CI minutes per month than most managed video APIs cost outright.
A multi-stage FFmpeg Dockerfile (and when it actually helps)
Multi-stage builds do not shrink apk add --no-cache ffmpeg. The package manager already installs runtime shared libraries only, and copying just /usr/bin/ffmpeg into a scratch stage produces a binary that immediately dies with Error loading shared library libavcodec.so.61 because Alpine's FFmpeg is dynamically linked against roughly 30 libraries.
Multi-stage pays off when what you're copying is static. Fetch a prebuilt static binary in stage one and ship only that:
FROM alpine:3.22 AS fetch
RUN apk add --no-cache curl xz tar
RUN curl -fsSL -o /tmp/ff.tar.xz \
https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz \
&& mkdir -p /tmp/ff && tar -xJf /tmp/ff.tar.xz -C /tmp/ff --strip-components=1 \
&& install -m 0755 /tmp/ff/bin/ffmpeg /tmp/ff/bin/ffprobe /out/ 2>/dev/null \
|| (mkdir -p /out && install -m 0755 /tmp/ff/bin/ffmpeg /tmp/ff/bin/ffprobe /out/)
FROM alpine:3.22
RUN apk add --no-cache gcompat font-dejavu fontconfig
COPY --from=fetch /out/ffmpeg /out/ffprobe /usr/local/bin/
RUN adduser -D -u 10001 ffuser
USER ffuser
ENTRYPOINT ["ffmpeg"]
Two things to note. gcompat is Alpine's glibc compatibility shim; the BtbN builds are static, but adding it costs under 1 MB and removes a whole class of "file not found" errors that are really loader errors. And USER ffuser matters because the default is root, which means every file FFmpeg writes to your mounted volume comes out owned by root on the host.
Common pitfalls when running FFmpeg in a container
These are the failures that show up after the Dockerfile builds clean.
- Exit code 137. That's 128 + SIGKILL(9): the kernel OOM killer took your process. Docker Desktop's VM has its own memory ceiling, and any
--memorylimit applies to the whole container. Heavy filter graphs and 4K decode blow past 512 MB fast. Raise the limit or lower the resolution before you blame FFmpeg. The same failure mode on hosted platforms is covered in self-hosting FFmpeg on Railway. drawtextandsubtitlesfail with "Cannot load default config file". The container has no fonts. Addfont-dejavuandfontconfigon Alpine, orfonts-dejavu-coreandfontconfigon Debian. Full filter syntax is in the drawtext guide.Unknown encoder 'libfdk_aac'. No distro package ships it. Building with--enable-libfdk-aacrequires--enable-nonfree, and a nonfree FFmpeg binary cannot be legally redistributed, which means you can't push that image to a public registry. Use the nativeaacencoder at 128k to 192k instead; the quality gap at those bitrates is small.exec format erroron deploy. You built on an Apple Silicon Mac, so the image is arm64 and your server is amd64. Build withdocker build --platform=linux/amd64 -t my-ffmpeg .or usedocker buildx build --platform linux/amd64,linux/arm64.- The container fills its disk mid-encode. Intermediate files, two-pass logs, and segment output all land in the container's writable layer unless you mount a volume. Mount one at
/tmpand give it real space. - Long jobs block your HTTP handler. A 20-minute encode inside a request cycle will hit a proxy timeout long before it finishes. You need a queue and a callback, which is the same pattern described in the n8n timeout fix.
What the container still doesn't give you
The Dockerfile is done in ten minutes. The production system around it is not. To run FFmpeg in Docker for real work, you also need a job queue so encodes don't run inline, worker autoscaling because CPU demand is spiky and encode CPU is the whole bill, persistent or object storage since container filesystems are ephemeral, retry and idempotency logic for jobs killed mid-encode, and monitoring that tells you a worker died at minute 18 of a 20-minute job. That's a media microservice. Most teams containerizing FFmpeg didn't set out to build one.
The full arithmetic on when that's worth owning is in FFmpeg in the cloud vs running FFmpeg yourself, and the same wall shows up with a different shape on serverless in FFmpeg on AWS Lambda.
Docker container vs one API call
Same job, both ways:
| FFmpeg in Docker | FFmpeg Micro | |
|---|---|---|
| Setup | Dockerfile, registry, base-image pinning | API key |
| Image size | 100 MB to 700 MB depending on codecs | none |
| Long jobs | your queue, your retries | submit job, poll or webhook |
| Scaling | you size the workers | no servers to run |
| Codec updates | rebuild and redeploy | managed |
The API version of the transcode above is a single request:
curl -X POST https://api.ffmpeg-micro.com/v1/jobs \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://example.com/input.mov",
"operation": "transcode",
"options": { "format": "mp4", "video_codec": "h264", "crf": 23 },
"webhook_url": "https://yourapp.com/hooks/ffmpeg"
}'
You get a job ID back, poll it or wait for the webhook, then download the output. Exact payload fields for each operation are in the docs. The same jobs run from n8n, Make, and Zapier, or from your AI agents through the MCP server.
When to keep the container: you're batch-processing on hardware you already pay for, you need a codec or hardware acceleration path that a hosted API doesn't expose, media can't legally leave your network, or your volume is low enough that a cron job on an existing box is genuinely simpler. Those are real reasons. "I don't want to install FFmpeg locally" is not one of them.
FAQ
Which Docker image should I use for FFmpeg?
Use alpine:3.22 with apk add --no-cache ffmpeg for FFmpeg 7.1 in the smallest footprint, or jrottenberg/ffmpeg:7-alpine if you want a maintained prebuilt image with a wider codec set and no build step. Switch to debian:bookworm-slim if anything else in your image needs glibc.
How big is an FFmpeg Docker image?
An Alpine base with the ffmpeg package lands around 100 MB on disk, though Docker Hub will report roughly 30 to 35 MB because it shows compressed size. Debian and Ubuntu bases start at 75 to 78 MB before FFmpeg, and a from-source build with full codec support can exceed 700 MB unless you use a multi-stage build to drop the toolchain.
Why does my FFmpeg container exit with code 137?
Exit 137 is 128 plus signal 9, meaning the kernel OOM killer terminated FFmpeg because the container hit its memory limit. Raise the limit with docker run --memory=2g, reduce the working resolution, or split the filter graph into stages.
Can I run FFmpeg in Docker on an Apple Silicon Mac?
Yes, and apk add ffmpeg works natively on arm64. The problem is deploying that image to an amd64 server, which fails with exec format error. Build with --platform=linux/amd64 or produce a multi-arch image with docker buildx.
Do I need Docker at all just to run FFmpeg jobs?
Only if you want to own the workers. If the goal is a video step inside an app or an automation, a hosted FFmpeg API removes the image, the queue, and the scaling work entirely, and you call it the same way from code, from n8n, or from an agent.
If your Dockerfile is the easy part and the queue is what you're dreading, skip both. Start on the free tier and see what the FFmpeg API does with your first encode.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

How to Install the FFmpeg Library for Audacity (Windows and Mac)
Install the Audacity FFmpeg library the right way: match avformat versions to your Audacity build, use the Locate dialog, and fix the macOS Homebrew mismatch.

How to Install FFmpeg (Windows, macOS, and Linux)
Install FFmpeg on Windows, macOS, or Linux with one command per OS, plus verify steps and PATH fixes for when your ffmpeg install isn't found by your shell.

Trim Silence From Video Automatically with FFmpeg (API)
How to remove silences from video automatically with FFmpeg silencedetect: detect, invert, and cut both streams in one pass without breaking lip sync.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free