Motionhooks bouwen: een Remotion-renderfarm achter één Docker-image
Hoe we een AI-videoplatform bouwden dat rendert met Remotion, publiceert naar negen platformen, en draait als één Docker-image over drie Kamal-rollen.
Videorendering breekt de aannames waarop de meeste webapplicaties gebouwd zijn. Een requestcyclus meet je in milliseconden; een render in minuten. Een webserver moet honderden gelijktijdige requests aankunnen; een renderproces wil een hele CPU en enkele gigabytes geheugen voor zichzelf.
Motionhooks zet een briefing om in een afgeronde socialevideo en publiceert die naar negen platformen. Deze post gaat over de architectuur die renderen en serveren laat samenleven zonder dat een van beide de ander verpest.
Eén image, drie rollen
De uitrol is één Docker-image dat als drie Kamal-rollen draait:
web— de Next.js-applicatie. Serveert het product, de marketingsite en de API.job— algemeen achtergrondwerk. Publiceren, OAuth-tokens verversen, geplande sweeps.render— Remotion-renders, en verder niets, op concurrency 1.
Eén image houdt de build eenvoudig en garandeert dat alle drie de rollen identieke code draaien. Splitsen per rol betekent dat een render een machine kan opslokken zonder de responstijd te raken.
Twee details daarin zijn het stelen waard.
Alleen web draait migraties. De entrypoint kijkt naar KAMAL_ROLE. Zouden alle drie de rollen bij het opstarten migraties draaien, dan zou een deploy drie processen tegen hetzelfde schema laten racen. Precies één rol verantwoordelijk maken haalt die race volledig weg:
# bin/docker-entrypoint (de vorm ervan)
if [ "$KAMAL_ROLE" = "web" ]; then
pnpm db:migrate
fi
exec "$@"
De renderrol weigert te starten zonder zijn credentials. assertRenderEnv faalt bij het opstarten in plaats van een worker gezond te laten opkomen die vervolgens elke job laat mislukken. Een worker die zijn werk niet kan doen, hoort niet in de pool — jobs stilzwijgend aannemen en laten falen is veel erger dan niet starten.
Scenario → manifest → render
De renderpijplijn heeft drie fasen, en juist die scheiding maakt hem debugbaar.
Een scenario is de intentie: het script, de shots, de voice-over, de muziek. Het is JSON, leesbaar voor mensen, en reviewbaar.
Een manifest is het uitgewerkte plan: elk asset gevonden of gegenereerd, elke duur berekend, elke parameter vastgelegd. Een scenario omzetten naar een manifest is waar het dure, foutgevoelige werk zit — beeldgeneratie, text-to-speech, muziek.
De render is deterministisch. Bij hetzelfde manifest produceert Remotion dezelfde video. Geen netwerkaanroepen, geen verrassingen.
Die grens betekent dat een mislukte render opnieuw kan zonder assets te hergenereren, en dat een slechte uitkomst te diagnosticeren is door het manifest te lezen in plaats van te gissen wat de AI gedaan heeft.
Remotion staat vastgepind op een exacte versie, met alle @remotion/*-pakketten op diezelfde versie. Een renderer is niet de plek voor een caret-range: een minor bump die de frametiming verandert is een stille visuele regressie die geen enkele test opvangt.
Waarom BullMQ en geen databasequeue
Meestal grijpen we naar de eenvoudigste queue die werkt, en vaak is dat de database. Hier niet.
Renderjobs zijn lang, geheugenhongerig en hebben strikte concurrency-controle op eigen hardware nodig. BullMQ op Redis geeft ons concurrency per queue, een aparte workergroep voor renders, en de mogelijkheid om de renderpool los van al het andere te schalen. WORKER_GROUP bepaalt wat een proces oppakt: general, render, of all voor lokale ontwikkeling.
Renders op concurrency 1 per worker draaien is bewust. Twee gelijktijdige renders op één machine gaan niet twee keer zo snel; ze vechten om geheugen en worden allebei trager, en als het geheugen opraakt is de faalmodus een OOM-kill die ze allebei meesleurt.
Publiceren naar negen platformen
Elk socialplatform heeft zijn eigen OAuth-dans, zijn eigen mediabeperkingen, zijn eigen idee van wat een API is. De verleiding is één grote conditional. Het onderhoudbare antwoord is een registry: één bestand per platform dat een gemeenschappelijke interface implementeert, en een index die platform naar implementatie vertaalt.
Een tiende platform toevoegen betekent één bestand schrijven en registreren. Verder leert niets in de codebase dat dat platform bestaat.
Dezelfde vorm geldt voor de OAuth-providers, die los staan van de publishers omdat authenticeren en posten verschillende levenscycli hebben — tokens verversen op hun eigen schema, lang na de post die ze nodig had.
OAuth-tokens voor negen platformen over veel workspaces is een serieus beveiligingsoppervlak, dus tokens worden versleuteld opgeslagen met AES-256-GCM in plaats van als platte tekst.
Credits, want renders kosten echt geld
Elke render kost GPU-tijd, modelaanroepen en opslag. Een vast abonnement met onbeperkt renderen is een bedrijf dat verlies maakt op zijn beste klanten.
Het creditsysteem zit tussen intentie en uitvoering: een actie krijgt een prijs, het saldo wordt gecontroleerd vóórdat het dure werk in de wachtrij gaat, en de afschrijving wordt op de job vastgelegd. Stripe doet de facturatie; het creditgrootboek is van ons, want “wat heeft deze gebruiker daadwerkelijk verbruikt” afstemmen tegen het wereldbeeld van een betaalprovider besteed je niet uit.
Het saldo controleren vóór het inplannen in plaats van na het renderen is wezenlijk. Het alternatief is ontdekken dat een klant door zijn credits heen is nadat je de GPU-tijd al hebt uitgegeven.
De stack
Next.js 16 met de App Router, React 19, Drizzle ORM op PostgreSQL, BullMQ op Redis, Remotion voor het renderen, Tailwind v4, next-intl voor Engels, Nederlands, Duits en Frans, Stripe voor facturatie.
De authenticatie is handgeschreven: jose voor tokens, scrypt voor wachtwoorden, AES-256-GCM voor tokenversleuteling, eigen TOTP voor 2FA. Dat is een bewuste keuze en geen lichtvaardige — je eigen auth bouwen is meestal een fout. Het verdient zijn plek wanneer het tenancy-model niet past bij wat kant-en-klare providers aannemen, en wanneer je sessiemodel exact moet aansluiten op een workspace-en-rollenstructuur waar het hele product omheen gebouwd is.
Mutaties lopen via server actions naast de code die ze gebruikt. API-routes bestaan alleen waar het moet: OAuth-callbacks, webhooks die de ruwe body nodig hebben (Stripe-handtekeningverificatie), en streaming chat.
Wat deze architectuur oplevert
De reden om dit allemaal te beschrijven is dat de vorm generaliseert. Elk product met een trage, dure, foutgevoelige kern — video, ML-inferentie, zware rapportgeneratie, PDF-pijplijnen — heeft hetzelfde probleem: dat werk kan niet in een request wonen, en het kan geen proces delen met iets dat responsief moet blijven.
Het patroon dat blijft werken: scheid de rollen, draai één image zodat ze niet uit elkaar lopen, geef de dure rol zijn eigen concurrency-regels, laat hem gesloten falen, en splits het dure werk in een resolvefase en een deterministische fase zodat retries goedkoop zijn.
Bouw je een product waar het moeilijke werk buiten de requestcyclus gebeurt? TTB Software doet architectuur en fractional CTO-werk voor precies deze klasse systemen.
Related Articles
Ledenboek bouwen: verenigingsbestuur vastleggen in Rails
Hoe we een ledenadministratieplatform voor Nederlandse verenigingen bouwden, en waarom het moeilijke deel niet de CRU...
Euromailing bouwen: waarom we onze eigen MTA draaien in plaats van een ESP door te verkopen
Hoe we een AVG-native e-mailmarketingplatform bouwden op Rails 8.1 en KumoMTA, en waarom het bezit van de verzendlaag...
Rails 2FA (TOTP): Twee-factor-authenticatie met ROTP, back-upcodes en versleutelde secrets
Rails 2FA met TOTP: implementeer twee-factor-authenticatie met ROTP, genereer back-upcodes, versleutel secrets met Ac...