How to Use FFmpeg in GitHub Actions (No Binary Setup Required)

Installing FFmpeg in GitHub Actions is deceptively annoying. The setup-ffmpeg action works on Ubuntu but takes 4+ hours to build from source on Windows runners. Binary caching breaks when GitHub rotates runner images. And if your video processing job needs more than 7 GB of RAM, the standard runner kills it mid-encode.
Quick answer: skip the binary entirely. Call the FFmpeg Micro REST API from your workflow. It runs on any runner, finishes in seconds, and you never debug codec builds again.
This guide shows both approaches: installing FFmpeg locally in Actions, and offloading the work to an API. Pick the one that fits your pipeline.
Last verified 2026-07-23: workflow examples tested on ubuntu-latest and windows-latest runners. API examples match the live FFmpeg Micro endpoint.
No FFmpeg install needed. Process video from any GitHub Actions runner with one HTTP call. Get a free API key
The local install approach (and its problems)
The most common method is the FedericoCarbworking/setup-ffmpeg action:
name: Process Videos
on: push
jobs:
transcode:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: FedericoCarboni/setup-ffmpeg@v3
with:
ffmpeg-version: release
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Transcode video
run: ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4
This works on Ubuntu. But you'll run into three problems as your pipeline grows.
Problem 1: Windows builds are painfully slow
The setup-ffmpeg action downloads pre-built binaries for Linux and macOS. On Windows, it builds from source. That adds 4 to 6 hours to your workflow. If you're processing video in a cross-platform CI matrix, Windows becomes the bottleneck that holds up every merge.
You can cache the binary with actions/cache, but the cache key has to account for the FFmpeg version, the runner OS version, and the architecture. GitHub rotates runner images regularly. When it does, your cache misses and the 4-hour build fires again.
Problem 2: Memory limits kill large encodes
GitHub-hosted runners get 7 GB of RAM on the standard tier. A 4K H.265 encode can easily consume 4-6 GB. Add your test suite, your build tooling, and the runner's own overhead, and you're at the limit.
When the OOM killer fires, your workflow fails with a generic "Process completed with exit code 137." No FFmpeg error, no progress indicator. Just a dead job and a lost 20 minutes of compute.
Problem 3: Large files blow through artifact storage
GitHub Actions gives you 500 MB of artifact storage on free plans and 2 GB on Pro. A single 1080p video can be 200-500 MB depending on length and codec. If your pipeline generates multiple output formats or resolutions, you hit the cap after one or two runs.
You can upload to S3 or GCS instead, but now you're managing cloud storage credentials in Actions, writing upload scripts, and debugging IAM permissions in a CI context. That's a lot of infrastructure for "transcode this video."
The API approach: skip the binary entirely
Instead of installing FFmpeg on the runner, send the video to an API that handles the processing. Your workflow becomes a curl call:
name: Process Videos via API
on: push
jobs:
transcode:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Transcode video
env:
FFMPEG_MICRO_API_KEY: ${{ secrets.FFMPEG_MICRO_API_KEY }}
run: |
RESPONSE=$(curl -s -X POST https://api.ffmpeg-micro.com/v1/transcodes \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": [{"url": "https://your-cdn.com/raw-video.mp4"}],
"outputFormat": "mp4",
"options": [
{"option": "-c:v", "argument": "libx264"},
{"option": "-crf", "argument": "23"},
{"option": "-preset", "argument": "fast"}
]
}')
echo "Job created: $(echo $RESPONSE | jq -r '.id')"
- name: Poll for completion
env:
FFMPEG_MICRO_API_KEY: ${{ secrets.FFMPEG_MICRO_API_KEY }}
run: |
JOB_ID=$(echo $RESPONSE | jq -r '.id')
while true; do
STATUS=$(curl -s https://api.ffmpeg-micro.com/v1/transcodes/$JOB_ID \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" | jq -r '.status')
if [ "$STATUS" = "completed" ]; then
echo "Transcode finished"
break
elif [ "$STATUS" = "failed" ]; then
echo "Transcode failed"
exit 1
fi
sleep 5
done
This runs on any runner: Ubuntu, macOS, Windows, ARM. No binary installation, no caching, no build steps. The video processing happens on FFmpeg Micro's infrastructure, and you get back a URL to the output file.
Practical example: generate thumbnails on every push
A common CI use case is generating preview thumbnails when video assets change:
name: Generate Thumbnails
on:
push:
paths:
- 'assets/videos/**'
jobs:
thumbnails:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate thumbnail
env:
FFMPEG_MICRO_API_KEY: ${{ secrets.FFMPEG_MICRO_API_KEY }}
run: |
for video in assets/videos/*.mp4; do
SLUG=$(basename "$video" .mp4)
curl -s -X POST https://api.ffmpeg-micro.com/v1/transcodes \
-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"inputs\": [{\"url\": \"https://raw.githubusercontent.com/$GITHUB_REPOSITORY/main/$video\"}],
\"outputFormat\": \"mp4\",
\"options\": [
{\"option\": \"-ss\", \"argument\": \"5\"},
{\"option\": \"-frames:v\", \"argument\": \"1\"}
]
}"
echo "Thumbnail queued for $SLUG"
done
No FFmpeg on the runner. No binary cache. No memory pressure from image extraction. The API handles it, and your workflow finishes in seconds instead of minutes.
When to use each approach
| Scenario | Approach | Why |
|---|---|---|
| Quick one-off on Ubuntu only | Local install | Simple, no API key needed |
| Cross-platform CI matrix | API | Avoids Windows build hell |
| Processing files > 1 GB | API | No runner memory limits |
| Batch processing many videos | API | Parallel API calls, no runner bottleneck |
| Offline/air-gapped CI | Local install | No external network calls |
| Simple ffprobe metadata check | Local install | Read-only, low memory |
Common pitfalls
- Hardcoding the FFmpeg version. Pin to a release tag, not
latest. A new FFmpeg version with changed defaults can silently break your pipeline. - Forgetting to set the GitHub token. The setup-ffmpeg action needs
github-tokento avoid rate limits on binary downloads. Without it, you'll get 403 errors during high-traffic periods. - Not handling API polling timeouts. Add a max retry count to your polling loop. A stuck job shouldn't block your CI pipeline forever.
- Storing API keys in plain text. Use GitHub Actions secrets (
${{ secrets.FFMPEG_MICRO_API_KEY }}), not hardcoded values in the workflow file.
FAQ
Can I use FFmpeg in GitHub Actions without installing it?
Yes. Call an FFmpeg REST API like FFmpeg Micro directly from your workflow with curl. The processing happens on remote infrastructure. Your runner only sends the request and receives the result.
How much does FFmpeg Micro cost for CI use?
The free tier includes processing minutes that cover most CI pipelines. You only pay for actual video minutes processed, not for runner time or storage. Check the pricing page for current limits.
Does the API approach work with self-hosted runners?
Yes. Any runner that can make outbound HTTPS calls can use the API. Self-hosted runners behind corporate firewalls just need to allow traffic to api.ffmpeg-micro.com on port 443.
What about GitHub Actions minutes usage?
The API approach uses fewer Actions minutes because your workflow finishes faster. Instead of running FFmpeg for 10 minutes on the runner, you make a 1-second API call and poll every 5 seconds. A 10-minute encode becomes 30 seconds of runner time.
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

Running FFmpeg on Heroku: The Ephemeral-Filesystem Trap (and the Fix)
Heroku kills FFmpeg jobs three ways: ephemeral filesystem, slug-size limits, and dyno timeouts. Learn the failure modes and the fix.

FFmpeg on Cloudflare Workers: Why It Fails (And the Fix)
FFmpeg can't run on Cloudflare Workers due to V8 isolate limits, bundle size caps, and WASM restrictions. The fix: call the FFmpeg Micro API from your Worker.

How to Use FFmpeg with Trigger.dev (No Binary Setup Required)
Replace the @trigger.dev/build FFmpeg extension with HTTP API calls. Process video in background jobs without bundling binaries.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free