8 Commits Deep: Fixing Remotion Video Renders on Railway
What started as a simple dependency cleanup turned into an 8-commit debugging marathon to get Remotion video rendering working on Railway's deployment platform.
The Simple Win First
I kicked off the session by replacing the uuid package with Node.js's built-in crypto.randomUUID(). One fewer dependency is always a good thing.
Then Railway Decided to Humble Me
The main event was getting Remotion video rendering working on Railway's Nixpacks environment. What I thought would be straightforward turned into a chain of Chrome/Chromium issues that required 8 commits to untangle:
The Problem Chain:
- Missing libnspr4.so — Remotion's bundled
chrome-headless-shellcouldn't find system libraries - Created nixpacks.toml with Chromium + all the required system deps (nss, nspr, gtk3, mesa, the whole gang)
- EBUSY errors —
npm ciwas running twice because of duplicate build phases - browserExecutable not taking effect — Had to explicitly set
browserExecutableandchromiumOptionsin bothselectCompositionandrenderMediacalls - Wrong Chromium path — Nixpacks puts binaries in
/nix/store/<hash>/bin/, not/usr/bin/chromium. Created afindChromiumExecutable()function that useswhichto find binaries on$PATH - "Old Headless mode removed" — Modern Chromium dropped the old
--headlessmode. Switched toungoogled-chromiumand addedheadless: trueto chromiumOptions - "Expected a positive number" — Added duration validation guards throughout the codebase
- The real culprit:
offthreadVideoCacheSizeInBytes: 0— Remotion requires a positive number, not zero. Removed the option entirely
The debug logging commit with verbose: true and proper try/catch around renderMedia was what finally revealed the actual error source. Sometimes you need to make the code scream to hear what it's trying to tell you.
Key Decisions That Saved My Sanity
- Using
execFileSync("which", [bin])over hardcoded paths — Nix uses content-addressed storage, so paths are dynamic - ungoogled-chromium over regular chromium — supports the headless-shell mode that modern Chrome dropped
- Defense in depth for durations — validation in TTS (min 2s), server.js (default 4s per scene), and components (Math.max 1 frame)
The Mess I Left Behind
I've got some cleanup to do:
- Debug logging is still active (intentionally, for the next Railway test)
- Two junk files sitting in the repo root from what looks like an accidental paste
- A script rename that's uncommitted
What's Next
The offthreadVideoCacheSizeInBytes: 0 fix was the last piece of the puzzle. Now I need to trigger a test render on Railway to confirm everything works end-to-end. If it does, I'll clean up the debug logging and call this deployment saga complete.
Sometimes the simplest features require the most complex solutions. But hey, that's why we build in public — so you can see the real mess behind the magic.