Ctrl K

Render a Manim Scene with Docker

Render a Manim Community scene through the official Docker image, bind mounting the project so the output video lands locally.

Manim pulls in heavy system dependencies (Cairo, Pango, ffmpeg, and LaTeX for any math text). Running it through the official manimcommunity/manim image keeps that whole toolchain off the host. A bind mount maps the project folder into the container, so the rendered video is written straight back to the local folder.

Overview

  • Keep the scene file and a small compose file in one folder.
  • The compose service mounts the current folder to /manim inside the container.
  • Render with docker compose run, passing the full manim command.
  • The output mp4 is written into media/ in the same local folder.

Compose file

The service only needs the image, a working directory, and a bind mount. The official image already contains Manim and its system dependencies.

services:
  manim:
    image: manimcommunity/manim:stable
    working_dir: /manim
    volumes:
      - .:/manim

Sample scene

This scene draws a risk vs return plane, places a few assets, and adds a capital market line. It uses Text rather than MathTex, so the render does not depend on LaTeX.

from manim import *


class Video01_RiskAndReturn(Scene):
    def construct(self):
        title = Text("Risk and Return", font_size=48)
        self.play(Write(title))
        self.wait(0.5)
        self.play(title.animate.to_edge(UP))

        axes = Axes(
            x_range=[0, 10, 2],
            y_range=[0, 10, 2],
            x_length=8,
            y_length=5,
            axis_config={"include_numbers": True},
        )
        x_label = axes.get_x_axis_label(Text("Risk (volatility)", font_size=24))
        y_label = axes.get_y_axis_label(Text("Expected return", font_size=24))
        self.play(Create(axes), FadeIn(x_label), FadeIn(y_label))

        # A few assets positioned by (risk, return).
        assets = {
            "Bonds": (2, 3),
            "Index": (5, 6),
            "Tech": (8, 8),
        }
        dots = VGroup()
        labels = VGroup()
        for name, (risk, ret) in assets.items():
            dot = Dot(axes.c2p(risk, ret), color=BLUE)
            label = Text(name, font_size=20).next_to(dot, UP, buff=0.1)
            dots.add(dot)
            labels.add(label)
        self.play(LaggedStartMap(GrowFromCenter, dots, lag_ratio=0.3))
        self.play(LaggedStartMap(FadeIn, labels, lag_ratio=0.3))

        # Capital market line drawn from a risk-free point through the market.
        rf = Dot(axes.c2p(0, 2), color=YELLOW)
        rf_label = Text("Risk-free", font_size=20).next_to(rf, LEFT, buff=0.1)
        cml = Line(axes.c2p(0, 2), axes.c2p(10, 9), color=GREEN)
        self.play(FadeIn(rf), FadeIn(rf_label))
        self.play(Create(cml))
        self.wait(1)

The class name is the scene name passed on the command line. c2p (coordinates to point) maps data values like (risk, return) onto axis positions, so the dots and line are placed in axis space rather than raw screen coordinates.

Render

Run from the folder holding both files. The first run pulls the image once, then renders.

docker compose run --rm manim \
  manim -qh --fps 30 --resolution "1920,1080" video01_risk_and_return.py Video01_RiskAndReturn
# --rm: remove the one-off container after it exits
# -qh: high quality preset (1080p, 60 fps by default)
# --fps 30: override the preset frame rate to 30
# --resolution "1920,1080": render at 1920x1080
# video01_risk_and_return.py: the scene file
# Video01_RiskAndReturn: the Scene class to render

The local preview flag -p is dropped on purpose. It opens the result in a video player, and the headless container has none. Open the output file directly instead.

Output

The video is written into the bind mounted folder, so it is available locally without a copy step. The path encodes the scene file, resolution, and frame rate.

media/videos/video01_risk_and_return/1080p30/Video01_RiskAndReturn.mp4
  • The image runs as uid 1000, so output files are owned by a typical single-user host account, not root. If the host user is not uid 1000, add a user mapping to the compose service.
  • stable is a moving tag. Pin a version tag such as manimcommunity/manim:v0.20.1 when a reproducible render is needed.
  • A scene that uses MathTex or Tex needs LaTeX. The official image already includes it, so no extra setup is required.