DEV Community

Javid Jamae
Javid Jamae

Posted on • Originally published at ffmpeg-micro.com

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

Originally published at ffmpeg-micro.com

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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-token to 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.

Top comments (0)