Skip to content

Install ffmpeg in the image that actually ships - #276

Merged
ralyodio merged 1 commit into
masterfrom
fix-ffmpeg-in-shipped-image
Sep 24, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix-ffmpeg-in-shipped-image

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The render pipeline would have failed on every job in production. One line, but it's the difference between the feature working and every render dying at the first spawn.

What was wrong

Package B added ffmpeg to worker/Dockerfile. That image is only built when the worker runs as its own Railway service — and on this project it doesn't. There is exactly one service (crawlproof.com); the worker is colocated with the Next.js app, launched by start.sh inside the container built from the root Dockerfile, which railway.json names explicitly:

"build": { "builder": "DOCKERFILE", "dockerfilePath": "Dockerfile" }

That root image installs pandoc tini and no ffmpeg. So the binaries the renderer needs were installed into an image nobody builds, and every ad_video_jobs row would have gone straight to failed with an ffmpeg spawn error.

Why CI couldn't catch it

Nothing in the test suite or typechecks says anything about which Dockerfile Railway builds. The CI job installs its own ffmpeg on the runner (added in package B, and genuinely useful — it's what proves the encoder works), which makes the green check actively misleading here: the pipeline passes on a machine that has ffmpeg while shipping to one that doesn't.

The fix

Adds ffmpeg to the runtime stage of the root Dockerfile, next to the pandoc the worker already needed for the same reason. worker/Dockerfile keeps its copy so the separate-service deployment stays correct if it's ever used.

Verified alongside this: REDIS_URL is set on the service, so the BullMQ consumer will actually pick up queued jobs once the image ships; start.sh runs the worker in-container; and the runtime stage already copies lib/, worker/, tsconfig.json and node_modules, so the @/ alias the render modules use resolves under tsx.

No code change, no test change — this is purely the deployed image.

Package B added ffmpeg to worker/Dockerfile. That image is only built when the
worker runs as its own Railway service, and on this project it does not: the
worker is colocated with the Next.js app and started by start.sh inside the
container built from the ROOT Dockerfile, which railway.json names explicitly.

So the binaries the renderer needs were installed into an image nobody builds,
and every video render job would have died at the first ffmpeg spawn with the
job row left `failed`. Nothing in CI could catch this — the test suite installs
its own ffmpeg on the runner, and typechecks and unit tests say nothing about
which Dockerfile Railway uses.

Adds ffmpeg to the runtime stage of the root Dockerfile, next to the pandoc the
worker already needed for the same reason. worker/Dockerfile keeps its copy so
the separate-service deployment stays correct if it is ever used.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

48 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM js-dynamic-code-execution lib/crawl-limits.ts:67
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:14
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:24
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:20
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:24
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:25
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:26
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:31
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:35
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 9ea4cd9 into master Sep 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant