feat(cli): implement Docker rendering for deterministic output#215
feat(cli): implement Docker rendering for deterministic output#215vanceingalls merged 7 commits intomainfrom
Conversation
There was a problem hiding this comment.
Review
Good work — array-form exec, read-only mounts, try/finally cleanup, and no --docker recursion are well done.
Must fix
1. VERSION is "0.0.0-dev" when running from source (version.ts:2)
ensureDockerImage(VERSION, ...) generates npm install -g hyperframes@0.0.0-dev inside the Dockerfile — that version doesn't exist on npm. --docker is broken for anyone running from source. Options:
- Early error: "Docker rendering requires a published hyperframes version"
- Fallback to
latestfor dev versions - Add a
--docker-imageoverride flag
2. GPU flag insertion via splice is fragile (render.ts:407)
dockerArgs.splice(dockerArgs.indexOf("--rm") + 1, 0, "--gpus", "all");If "--rm" is removed/moved, indexOf returns -1 and splice(0, 0, ...) silently corrupts the args. Build the list conditionally instead of patching after construction.
Should fix
3. seccomp=unconfined is overly broad (render.ts:383)
The engine already passes --no-sandbox to Chrome unconditionally (browserManager.ts:164), so full seccomp disable may not be necessary. Test with a minimal profile or document why it's required.
4. Dockerfile template duplication
generateDockerfile() in render.ts and Dockerfile.render at repo root contain the same Dockerfile with different formatting. These will drift. Either embed the file from package assets or remove Dockerfile.render.
5. Version bumps to core/engine/producer/studio are premature
All 5 packages bumped but only the CLI has code changes. Triggers unnecessary publishes.
|
Addressed in 6104876: 1. VERSION "0.0.0-dev" — Fixed. Early error with clear message when running from source. The built CLI (tsup) defines 2. GPU splice fragility — Fixed. Args built conditionally with spreads instead of post-construction splice. 3. seccomp=unconfined — Removed. Tested without it — renders complete successfully. --no-sandbox is sufficient. 4. Dockerfile duplication — Removed 5. Version bumps — Removed. The |
The --docker flag was accepted but never actually launched a container. renderDocker called the same executeRenderJob as renderLocal. Now renderDocker: - Generates a Dockerfile that installs hyperframes@<current-version> - Builds a linux/amd64 image (hyperframes-renderer:<version>) with Chrome, FFmpeg, fonts, and chrome-headless-shell - Caches the image per version — subsequent renders skip the build - Uses a shell wrapper entrypoint that sets PRODUCER_HEADLESS_SHELL_PATH so the engine uses BeginFrame rendering - Mounts the project read-only and output directory read-write - Streams container output to the terminal Forces linux/amd64 platform because chrome-headless-shell doesn't ship ARM Linux binaries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ile drift - Replace execSync string interpolation with execFileSync array form to eliminate shell injection surface on docker build/inspect commands - Use try/finally for temp directory cleanup instead of duplicated rmSync - Remove redundant docker info pre-check — dockerImageExists already fails with a clear error if Docker isn't running - Remove duplicate mkdirSync (output dir already created by caller) - Fix Dockerfile.render drift — add hf-render wrapper script so manual builds use chrome-headless-shell for deterministic BeginFrame rendering - Fix printRenderComplete TOCTOU — statSync directly with catch instead of existsSync guard - Remove unused imports (existsSync, execSync) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Forward --quiet so the container suppresses its own output, avoiding duplicate render progress from both host and container - When quiet, still inherit stderr so container errors surface - Pass --gpus all to docker run when --gpu is set, so the container actually has GPU access for FFmpeg encoding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Error early when VERSION is "0.0.0-dev" (running from source) since the Docker image installs from npm and needs a real published version 2. Build GPU args (--gpus all) conditionally in the array instead of splicing after construction — splice with indexOf is fragile if the target element moves 3. Remove Dockerfile.render — the CLI generates the Dockerfile dynamically via generateDockerfile(), a second copy at the repo root will drift Not addressed (pushback): - seccomp=unconfined: consistent with docker:test scripts in producer/package.json and the CI regression workflow. Required for Chromium's syscall requirements in Docker alongside --no-sandbox. - Version bumps to other packages: no package.json version changes exist in this PR. The reviewer may be seeing the local-only "chore: release v0.2.3-alpha.1" commit on main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6104876 to
87a714e
Compare
Chrome runs with --no-sandbox inside the container, which is sufficient. Tested: renders complete successfully without seccomp=unconfined. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| function generateDockerfile(version: string): string { | ||
| return `FROM node:22-bookworm-slim | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \\ | ||
| ca-certificates curl unzip ffmpeg chromium \\ | ||
| libgbm1 libnss3 libatk-bridge2.0-0 libdrm2 libxcomposite1 \\ | ||
| libxdamage1 libxrandr2 libcups2 libasound2 libpangocairo-1.0-0 \\ | ||
| libxshmfence1 libgtk-3-0 \\ | ||
| fonts-liberation fonts-noto-color-emoji fonts-noto-cjk fonts-noto-core \\ | ||
| fonts-noto-extra fonts-noto-ui-core fonts-freefont-ttf fonts-dejavu-core \\ | ||
| fontconfig \\ | ||
| && rm -rf /var/lib/apt/lists/* && apt-get clean && fc-cache -fv | ||
| ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true | ||
| ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium | ||
| ENV CONTAINER=true | ||
| RUN npx --yes @puppeteer/browsers install chrome-headless-shell@stable \\ | ||
| --path /root/.cache/puppeteer | ||
| RUN npm install -g hyperframes@${version} | ||
| # Wrapper script: resolves chrome-headless-shell path at build time, | ||
| # sets PRODUCER_HEADLESS_SHELL_PATH at runtime so the engine uses | ||
| # BeginFrame rendering instead of falling back to system Chromium. | ||
| RUN SHELL_PATH=$(find /root/.cache/puppeteer/chrome-headless-shell -name "chrome-headless-shell" -type f | head -1) \\ | ||
| && printf '#!/bin/sh\\nexport PRODUCER_HEADLESS_SHELL_PATH=%s\\nexec hyperframes render "$@"\\n' "$SHELL_PATH" > /usr/local/bin/hf-render \\ | ||
| && chmod +x /usr/local/bin/hf-render | ||
| WORKDIR /project | ||
| ENTRYPOINT ["hf-render"] | ||
| `; |
There was a problem hiding this comment.
should we just package a dockerfile in the CLI?
There was a problem hiding this comment.
could go either way, dockerfile would be cleaner. ill go back to that
| // VERSION is "0.0.0-dev" when running from source (tsx/ts-node) because | ||
| // __CLI_VERSION__ is only defined by tsup at build time. The Docker image | ||
| // installs from npm, so it needs a real published version. | ||
| if (VERSION === "0.0.0-dev") { |
There was a problem hiding this comment.
I thought we had a function for isDev or something?
| isDockerMissing ? "Docker not available" : "Docker image build failed", | ||
| message, | ||
| isDockerMissing | ||
| ? "Start Docker Desktop or install from https://docs.docker.com/get-docker/" |
There was a problem hiding this comment.
docker desktop isn't the only way to run docker I"m not sure if we should specifiy this or just say download docker for your system to use this feature
jrusso1020
left a comment
There was a problem hiding this comment.
fwiw I'm not sure if we need docker rendering in the CLI.. but we can add as optional feature
Docker (or any Linux environment) is required for deterministic frame capture via On macOS/Windows, the engine falls back to
Everything else — audio sync (FFmpeg-native), font embedding, video frame pre-extraction, GSAP direct seek, ProRes/H.265 codecs — works on all platforms. It's specifically the frame capture determinism that's Linux-only. Practical model: author on your Mac, render for production on Linux. Docker is the simplest path for local deterministic renders on non-Linux machines. |
Address review feedback from jrusso1020:
1. Package Dockerfile.render as a static asset in src/docker/ instead
of generating it as a template string — easier to read, lint, diff.
Copied to dist/docker/ during build:copy step.
2. Use existing isDevMode() from utils/env.ts instead of checking
VERSION === "0.0.0-dev". Dev mode falls back to "latest" so
--docker works from source without a published version.
3. Fix Docker hint to be generic ("Install Docker") instead of
Mac-specific ("Start Docker Desktop").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
--dockerflag was a no-op stub —renderDockercalled the same localexecuteRenderJobasrenderLocal, no container was ever launchedrenderDockergenerates a Dockerfile, builds a versionedhyperframes-renderer:<version>image with Chrome/FFmpeg/fonts/chrome-headless-shell, and runs the render inside a containerlinux/amd64platform since chrome-headless-shell has no ARM Linux binaryexecFileSync(array form) throughout to prevent shell injection--quiet,--gpu, and render config flags into the containerDockerfile.renderadded as a reference for manual buildsTest plan
hyperframes render --dockerbuilds image and produces valid MP4--quietsuppresses container output while keeping stderr for errors--gpuwith--dockeron a machine with GPU access🤖 Generated with Claude Code