CASE STUDY · 7 MIN READ ·

Building Motionhooks: A Remotion Render Farm Behind One Docker Image

How we built an AI video platform that renders with Remotion, publishes to nine platforms, and runs as a single Docker image across three Kamal roles.

Building Motionhooks: A Remotion Render Farm Behind One Docker Image

Video rendering breaks the assumptions most web applications are built on. A request cycle is measured in milliseconds; a render is measured in minutes. A web server should handle hundreds of concurrent requests; a render process wants a whole CPU and several gigabytes of memory to itself.

Motionhooks turns a brief into a finished social video and publishes it to nine platforms. This post is about the architecture that makes rendering and serving coexist without either one ruining the other.

One image, three roles

The deployment is a single Docker image running as three Kamal roles:

  • web — the Next.js application. Serves the product, the marketing site and the API.
  • job — general background work. Publishing, OAuth token refresh, scheduled sweeps.
  • render — Remotion renders, and nothing else, at concurrency 1.

One image keeps the build simple and guarantees that all three roles run identical code. Splitting by role means a render can consume a machine without touching request latency.

Two details in that setup are worth stealing.

Only web runs migrations. The entrypoint gates on KAMAL_ROLE. If all three roles ran migrations on boot, a deploy would race three processes against the same schema. Making exactly one role responsible removes the race entirely:

# bin/docker-entrypoint (shape of it)
if [ "$KAMAL_ROLE" = "web" ]; then
  pnpm db:migrate
fi
exec "$@"

The render role refuses to boot without its credentials. assertRenderEnv fails closed at startup rather than letting a worker come up healthy and then fail every job it picks up. A worker that cannot do its job should not be in the pool — silently accepting and failing jobs is far worse than not starting.

Scenario → manifest → render

The rendering pipeline has three stages, and the separation is what makes it debuggable.

A scenario is the intent: the script, the shots, the voice-over, the music. It is JSON, human-readable, and reviewable.

A manifest is the resolved plan: every asset located or generated, every duration computed, every parameter pinned. Turning a scenario into a manifest is where the expensive, failure-prone work lives — image generation, text-to-speech, music.

The render is deterministic. Given the same manifest, Remotion produces the same video. No network calls, no surprises.

That boundary means a failed render can be retried without regenerating assets, and a bad output can be diagnosed by reading the manifest instead of guessing what the AI did.

Remotion is pinned to an exact version, with every @remotion/* package on the same version. Renderers are not the place for a caret range: a minor bump that changes frame timing is a silent visual regression that no test catches.

Why BullMQ and not a database queue

We generally reach for the simplest queue that works, and often that is the database. Here it was not.

Render jobs are long, memory-hungry, and need strict concurrency control on dedicated hardware. BullMQ on Redis gives us per-queue concurrency, a separate worker group for renders, and the ability to scale the render pool independently of everything else. WORKER_GROUP selects what a process picks up: general, render, or all for local development.

Running renders at concurrency 1 per worker is deliberate. Two concurrent renders on one box do not go twice as fast; they contend for memory and both get slower, and the failure mode when memory runs out is an OOM kill that takes both down.

Publishing to nine platforms

Every social platform has its own OAuth dance, its own media constraints, its own idea of what an API is. The temptation is a large conditional. The maintainable answer is a registry: one file per platform implementing a common interface, and an index that resolves platform to implementation.

Adding a tenth platform means writing one file and registering it. Nothing else in the codebase learns that the platform exists.

The same shape applies to the OAuth providers, kept separate from the publishers because authenticating and posting have different lifecycles — tokens refresh on their own schedule, long after the post that needed them.

OAuth tokens for nine platforms across many workspaces is a meaningful security surface, so tokens are encrypted at rest with AES-256-GCM rather than stored as plaintext strings.

Credits, because renders cost real money

Every render costs GPU time, model calls and storage. A flat subscription with unlimited rendering is a business that loses money on its best customers.

The credit system sits between intent and execution: an action is priced, the balance is checked before the expensive work is enqueued, and the debit is recorded against the job. Stripe handles billing; the credit ledger is ours, because reconciling “what did this user actually consume” against a payment provider’s view of the world is not something to outsource.

Checking the balance before enqueueing rather than after rendering matters. The alternative is discovering a customer is out of credit after you have already spent the GPU time.

The stack

Next.js 16 with the App Router, React 19, Drizzle ORM against PostgreSQL, BullMQ on Redis, Remotion for rendering, Tailwind v4, next-intl for English, Dutch, German and French, Stripe for billing.

Auth is hand-rolled: jose for tokens, scrypt for passwords, AES-256-GCM for token encryption, custom TOTP for 2FA. That is a deliberate choice and not one to make lightly — rolling your own auth is usually a mistake. It earns its place when the tenancy model does not fit what off-the-shelf providers assume, and when you need the session model to line up exactly with a workspace-and-role structure that the product is built around.

Mutations go through colocated server actions. The API routes exist only where they must: OAuth callbacks, webhooks that need the raw body (Stripe signature verification), and streaming chat.

What this architecture buys

The reason to describe all this is that the shape generalises. Any product with a slow, expensive, failure-prone core — video, ML inference, large report generation, PDF pipelines — faces the same problem: that work cannot live in a request, and it cannot share a process with the thing that has to stay responsive.

The pattern that keeps working: separate the roles, run one image so they cannot drift, give the expensive role its own concurrency rules, make it fail closed, and split the expensive work into a resolve phase and a deterministic phase so retries are cheap.


Building a product where the hard part happens outside the request cycle? TTB Software does architecture and fractional CTO work for exactly this class of system.

#remotion-production-rendering #kamal-multi-role-deploy #bullmq-render-queue #nextjs-multi-tenant-saas #ai-video-generation-platform #social-media-publishing-api

Related Articles

Last section. Then please call.

It's a phone call. That's the worst it can get.

No discovery deck. No 45-minute "qualification" call. 30 minutes, your problem, my opinion. If we're a fit, you'll know by minute 12.

Direct line — answered by Roger
+31 6 5123 6132
Mon–Fri, 09:00–18:00 CET · Currently available

OR
info@ttb.software