Skip to content

Render on a server

After this recipe you can render projects on a Linux server, in a container, on a GPU or on CPUs only, and work through a queue of render jobs from Node.js.

The server runs alpha2-host, the native renderer. It draws the same frames as the browser preview, because both run the same rendering code. See Determinism.

  • Linux x64 or arm64 with glibc 2.35 or later. The release archives are built for glibc, and musl systems such as Alpine are refused: npx @alpha2-sh/node install stops before it downloads anything, and a host started there fails with HOST_LOAD_FAILED. Debian 12 and Ubuntu 22.04 or later are new enough.
  • Node.js 22.21 or later (24.5 or later on Node 24) for @alpha2-sh/node, or Python 3.11.4 or later for the alpha2 Python package.
  • For GPU rendering, a Vulkan driver for the GPU. Hardware decoding uses Vulkan Video. NVENC on NVIDIA GPUs needs driver 530.41.03 or newer, and inside a container it needs the driver’s video capability.
  • For CPU rendering, nothing else: the CPU renderer opens no graphics driver.

The archive carries its own FFmpeg 8 libraries in lib/, so the image needs no FFmpeg package.

alpha2-host picks its renderer when it starts, from --renderer on export or the ALPHA2_RENDERER environment variable (which serve and the Node.js and Python clients read, along with their own renderer option):

Mode Renders on Fails when
auto (default) a hardware GPU if there is one, else the CPU never for lack of a GPU
gpu any GPU adapter, software ones included there is no adapter: GPU_INIT_FAILED
cpu the CPU renderer never for lack of a GPU

auto never renders on a software adapter (lavapipe, llvmpipe, WARP, SwiftShader), which draws the same pictures many times slower than the CPU renderer. A cloud VM with only a software Vulkan driver therefore renders on the CPU under auto.

The CPU renderer draws the same render graph as the GPU, and its pictures are the same bytes on every machine and for any number of threads. It uses every thread the process may run on; ALPHA2_RENDER_THREADS sets the number. It draws every built-in effect and transition, but a runtime WGSL effect needs a GPU and fails with EFFECT_ERROR on the CPU.

Every export result says which renderer drew it: renderer (gpu or cpu), rendererReason when auto chose the CPU, and rendererThreads on the CPU.

The FFmpeg in the archives is LGPL and has no software H.264 encoder. A server with no hardware H.264 encoder (a CPU-only machine, an arm64 server, an NVIDIA datacenter GPU such as A100 or H100) needs Cisco’s OpenH264, or another codec:

  • Install OpenH264 with npx @alpha2-sh/node install --openh264 (or python -m alpha2 install --openh264). It downloads Cisco’s binary from Cisco, as Cisco’s licence requires.
  • Or export a codec the runtime encodes on the CPU: VP9 or AV1 (--video-codec vp9 in an .mp4, .mkv or .webm), or mpeg4-part2.

getCapabilities() reports encode.h264.status (available, gpu-only or unavailable) with a reason and a remedy, before you render anything.

This image installs @alpha2-sh/node, the host archive of the same version, and OpenH264. alpha2’s own install tests run the host in node:22-slim and python:3.12-slim images.

Dockerfile
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# One cache for the build user and the run user.
ENV ALPHA2_HOST_CACHE_DIR=/opt/alpha2
RUN npx @alpha2-sh/node install && npx @alpha2-sh/node install --openh264
ENV ALPHA2_RENDERER=cpu
COPY . .
CMD ["node", "worker.mjs"]
  • npx @alpha2-sh/node install downloads the release archive for this package’s version and checks it against the SHA-256 the package pins.
  • The installs write to the cache directory. ALPHA2_HOST_CACHE_DIR makes it the same at build time and at run time, whatever user the container runs as.
  • Running the OpenH264 install in the image build is the download Cisco’s terms ask for. Do not copy the library into an image you publish.

For GPU rendering, start from an image with the GPU’s Vulkan driver, pass the GPU into the container, and leave ALPHA2_RENDERER unset (auto) or set it to gpu to fail fast without one.

Terminal window
alpha2-host export --renderer cpu \
--project job.json \
--asset clip-1=/data/in/clip-1.mp4 \
--font /data/fonts/Inter.ttf \
--out /data/out/job.mp4

The command prints the result as JSON on standard output and progress on standard error. It exits 0 on success, 1 on a failure, and 70 when the GPU device cannot be created. See the CLI reference.

alpha2-host conform checks the job’s media without rendering and without a GPU, and exits 1 when something blocks the export: a cheap check before a job takes a render slot.

Alpha2NodeClient runs each export in its own alpha2-host process. concurrency is how many run at once and so how many share the machine; more requests wait for a slot.

worker.mjs
import { Alpha2HostError, Alpha2NodeClient } from "@alpha2-sh/node";
import { isRetryableFailure } from "alpha2/export-retry";
const client = new Alpha2NodeClient({ concurrency: 2 });
// The end of everything the timeline shows.
function projectEnd(project) {
let end = 0;
for (const track of project.timeline.tracks) {
for (const clip of track.clips) end = Math.max(end, clip.timeline_range.end);
for (const cue of track.captions?.cues ?? []) end = Math.max(end, cue.timeline_range.end);
}
return end;
}
async function render(job) {
const { project } = job;
for (let attempt = 1; ; attempt++) {
try {
return await client.exportProject({
project,
assets: job.assets, // { "<source-id>": "/data/in/..." }
fonts: job.fonts,
startTicks: 0,
endTicks: projectEnd(project),
frameRate: project.settings.frame_rate,
output: job.output,
overwrite: true,
onProgress: ({ phase, completed, total }) => job.report?.(phase, completed, total),
});
} catch (error) {
if (!(error instanceof Alpha2HostError)) throw error;
if (attempt < 3 && isRetryableFailure(error.failure)) continue;
throw error; // invalid input, missing media, cancelled... retrying will not help
}
}
}
// Take jobs from your queue; here, an async iterable.
for await (const job of queue) {
render(job).then(job.done, job.fail);
}
  • The project is validated before a process starts. An invalid one is refused with Alpha2ProjectValidationError and its problems. Media that is missing or offline is refused with Alpha2ConformError and the conform report, before anything renders.
  • error.failure classifies every failure (lost device, out of memory, bad input…). isRetryableFailure from alpha2/export-retry is false for failures a retry cannot mend; decideRetry also suggests switching to the CPU renderer. Install alpha2 beside @alpha2-sh/node to import it.
  • A host the kernel’s out-of-memory killer ends fails with HOST_OOM_KILLED. Lower concurrency, or give the export smaller memory budgets.
  • signal (an AbortSignal) cancels an export: the host stops, removes its partial file, and the call rejects with Alpha2AbortError.
  • An output is written as <stem>.a2partial.<ext> beside the final path and moved into place when complete, so a reader never sees half a file.