n8n + ffmpeg

apk add ffmpeg doesn't work in the n8n image anymore

Since n8n 2.1.0 the official Docker image ships without a package manager, so the one-line Dockerfile everyone copied exits with apk: not found. n8n Cloud never had ffmpeg at all. Here is what changed, the Dockerfile that still works for self-hosters, and the no-image route for everyone else.

What changed

The official image has shipped without apk since 2.1.0 (December 2025). It is a hardened Alpine with the package manager removed, not distroless, so the container still has a shell but nothing to install with. The symptom is the same everywhere:

docker build
Step 3/4 : RUN apk add --no-cache ffmpeg
/bin/sh: apk: not found
ERROR: process "/bin/sh -c apk add --no-cache ffmpeg" did not complete successfully: exit code: 127

Two more things landed with 2.0: the Execute Command node is disabled by default in self-hosted instances, and on n8n Cloud it does not exist. So even with ffmpeg in the image you need to re-enable the node before a workflow can call it (the 2.0 migration notes cover the NODES_EXCLUDE setting).

Self-hosted: the Dockerfile that still builds

Copy apk back in from a plain Alpine stage of the same Alpine version as the n8n tag, install, and drop back to the node user:

Dockerfile
FROM alpine:3.24 AS alpine
FROM docker.n8n.io/n8nio/n8n:latest
COPY --from=alpine /sbin/apk /sbin/apk
COPY --from=alpine /usr/lib/libapk.so* /usr/lib/
USER root
RUN apk add --no-cache ffmpeg
USER node
  • Built on 2026-09-17 against n8n 2.39.6: ffmpeg 8.1.2, n8n still starts as node, the image grows from 270 to 313 MB.
  • The Alpine version has to match the one inside the n8n image. Check with cat /etc/os-release in a running container before you pick the first line.
  • It breaks the day n8n bumps its base image. Pin the n8n tag and budget a rebuild for every upgrade.
  • Then re-enable Execute Command, keep it off the public internet, and write the ffmpeg commands yourself.

Fine for a home lab. Less fine when the video step is the only reason you own a custom image.

n8n Cloud, or no image to maintain: one HTTP call

The step after the video generator is boring and always the same: make the clip 9:16, put the channel logo on it, fix the loudness, maybe burn in captions. Brand-Pipe does exactly that behind one HTTP Request node, so it works on n8n Cloud, on Hostinger, on a locked-down instance, anywhere the node can make a POST.

curl
curl -X POST https://api.brand-pipe.com/v1/process \
  -H "X-API-Key: bp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"video_url": "https://example.com/raw.mp4",
       "logo_url": "https://example.com/logo.png",
       "format": "vertical"}'

The answer is a JSON envelope with a signed download link; the ready-made workflow turns that into binary data for your upload node.

  1. Import n8n-brand-pipe.json (Manual Trigger, Set, HTTP Request, IF, download).
  2. Create a Header Auth credential named Brand-Pipe API Key: header X-API-Key, value your key.
  3. Replace the Set video node with your generator's output and connect the binary output to YouTube, TikTok or storage. n8n-brand-pipe-youtube.json is the scheduled version with a YouTube Shorts upload, shipped disabled so you can test before the OAuth setup.

The loudness part is the one that bites. A 9-second GoPro clip with wind on a covered mic measured -70 LUFS, basically silence. Plain loudnorm pulled it up to -12.3 LUFS, which is 58 dB of hiss. Brand-Pipe measures first and leaves anything below -50 LUFS alone. If you normalise with ffmpeg yourself, add that check.

Tips that apply to any video API in n8n

  • Turn on Never Error in the HTTP Request node options. Otherwise a 4xx throws and your IF node never sees the error message.
  • Retry On Fail with a paid API can charge you twice when only the response got lost. Send an Idempotency-Key header with {{ $execution.id }}-{{ $itemIndex }}. With just the execution id every item in the run gets the same key.
  • For longer clips don't keep the request open. Submit a job, put a Wait node on Resume on webhook call with method POST (the default is GET) and send {{ $execution.resumeUrl }} as callback URL. Set a wait limit, or a lost callback parks the execution forever. n8n-brand-pipe-jobs.json is that pattern against POST /v1/jobs.
  • On the download node set Response Format to File. Autodetect works when the host sends a video content type; not every host does.

Pricing

25 credits free, no card. One credit per video, two if it burns in captions, then €9 for 250 credits. Credits don't expire. Failed renders are refunded automatically (except the rare one that hits the encode time limit). Sync is up to 60 s of output, jobs up to 10 min, 200 MB per source. Renders run in Frankfurt, files are stored in the EU and deleted after 24 h.

Questions or a case that doesn't fit? support@brand-pipe.com