DEV Community

nareshipme
nareshipme

Posted on

How to Run Remotion Inside a Next.js App Without Webpack Losing Its Mind

Remotion is a fantastic library for programmatic video rendering — you write React components and it renders them to MP4 using Chromium. But if you're embedding it inside a Next.js 15 App Router project, you'll quickly discover that Remotion and Next.js's bundler do not get along out of the box.

Here's every gotcha we hit, and exactly how to fix them.


The Problem

Remotionuses Node.js-only APIs (child_process, fs, native bindings for Chromium) and assumes a standalone Node environment. When Next.js tries to bundle your server-side code with webpack, it chokes on these imports:

Module not found: Can't resolve 'puppeteer-core'
Error: cannot find module '@remotion/renderer/...'
Enter fullscreen mode Exit fullscreen mode

Or worse — it silently tree-shakes something critical and you get mysterious render failures in production.


Fix 1: Externalize Remotion in next.config.ts

Tell Next.js's webpack to leave Remotion packages to the Node.js runtime instead of trying to bundle them:

// next.config.ts
const nextConfig: NextConfig = {
  serverExternalPackages: [
    '@remotion/renderer',
    '@remotion/bundler',
    '@remotion/core',
    'remotion',
  ],
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

In Next.js 15, experimental.serverComponentsExternalPackages was promoted to serverExternalPackages (no more experimental. prefix).


Fix 2: Render via a Spawned Script, Not an API Route

Even with externalization, calling Remotion's renderMedia() directly from an App Router API route can cause issues — especially in edge runtimes or when the bundler partially processes the file.

The clean solution: keep your Remotion composition in an isolated folder (src/remotion/) and render it via a spawned child process.

// src/lib/render-video.ts
import { spawn } from 'child_process';
import path from 'path';

export async function renderCaptionedVideo(
  videoUrl: string,
  captions: Caption[],
  outputPath: string
): Promise<string> {
  return new Promise((resolve, reject) => {
    const scriptPath = path.resolve(process.cwd(), 'src/scripts/render.mjs');
    const args = [
      scriptPath,
      videoUrl,
      JSON.stringify(captions),
      outputPath,
    ];

    const child = spawn('node', args, { stdio: 'inherit' });

    child.on('exit', (code) => {
      if (code === 0) resolve(outputPath);
      else reject(new Error(`Remotion renderer exited with code ${code}`));
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

And the standalone render script:

// src/scripts/render.mjs
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';

const [,, videoUrl, captionsJson, outputPath] = process.argv;
const captions = JSON.parse(captionsJson);

const bundled = await bundle({ entryPoint: './src/remotion/index.tsx' });
const composition = await selectComposition({
  serveUrl: bundled,
  id: 'CaptionedVideo',
  inputProps: { videoUrl, captions },
});

await renderMedia({
  composition,
  serveUrl: bundled,
  codec: 'h264',
  outputLocation: outputPath,
});

console.log('Render complete:', outputPath);
Enter fullscreen mode Exit fullscreen mode

This script runs completely outside Next.js's control — no bundling conflicts, no edge runtime surprises.


Fix 3: Exclude src/remotion from Next.js TypeScript

Remotioncompositions are React components that target the browser DOM. If your root tsconfig.json (which Next.js uses) includes src/remotion, TypeScript will complain about mismatched lib targets, browser-only APIs, etc.

Exclude it in your main tsconfig:

// tsconfig.json
{
  "exclude": ["node_modules", "src/remotion", "src/scripts"]
}
Enter fullscreen mode Exit fullscreen mode

And give the Remotion folder its own config:

// src/remotion/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "noEmit": true
  },
  "exclude": []
}
Enter fullscreen mode Exit fullscreen mode

Run type-checks for Remotion separately: tsc -p src/remotion/tsconfig.json.


Fix 4: Use HTTPS URLs, Not Local File Paths

Remotionrenders via Chromium. Chromium cannot load a bare local path like /tmp/video.mp4 — it needs either a file:// URL or an HTTP(S) URL.

// ❌ Doesn't work in Chromium
const videoSrc = '/tmp/uploads/video.mp4';

// ✅ Works in local dev
const videoSrc = `file://${path.resolve('/tmp/uploads/video.mp4')}`;

// ✅✅ Works everywhere, including Docker + cloud workers
const videoSrc = await generatePresignedUrl(r2Key); // https://...
Enter fullscreen mode Exit fullscreen mode

If you're running on a cloud worker (Railway, Fly, etc.) — go with presigned URLs. You get the HTTPS URL from your object storage (Cloudflare R2, S3, etc.) and pass it directly to the composition. No volume mounts needed.


Fix 5: Docker Setup for Remotion

Remotionneeds Chromium and its shared libraries. Here's a minimal Dockerfile setup that works on Railway:

FROM node:20-slim

# Chromium + system deps for Remotion rendering
RUN apt-get update && apt-get install -y \
  chromium \
  ffmpeg \
  fonts-liberation \
  libatk-bridge2.0-0 \
  libatk1.0-0 \
  libcups2 \
  libdrm2 \
  libgbm1 \
  libgtk-3-0 \
  libnss3 \
  libxcomposite1 \
  libxdamage1 \
  libxrandr2 \
  xdg-utils \
  && rm -rf /var/lib/apt/lists/*

# Tell Remotion/Puppeteer where Chromium is
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV CHROME_EXECUTABLE_PATH=/usr/bin/chromium
Enter fullscreen mode Exit fullscreen mode

Make sure to set CHROME_EXECUTABLE_PATH — Remotion uses this env var to locate the browser binary.


Summary

Problem Fix
Webpack bundling Remotion serverExternalPackages in next.config.ts
API route render conflicts Spawn a standalone Node.js script
TypeScript lib mismatch Exclude src/remotion from root tsconfig
Chromium can't load local paths Use file:// URIs or presigned HTTPS URLs
Missing Chromium in Docker Install chromium + deps, set CHROME_EXECUTABLE_PATH

The key mental model: Remotion is not a Next.js library. It's a Node.js CLI tool that happens to be usable programmatically. Treat it that way — run it outside the Next.js process, feed it HTTPS URLs, and let Node handle it directly. Your webpack config will thank you.


Working on a Next.js video platform and hit a different Remotion quirk? Drop it in the comments.

Top comments (0)