Scaling
Scale a self-hosted Nodaro beyond one container. Split the API, media workers, render workers and orchestrator, and tune concurrency, Redis and storage.
Scaling a self-hosted Nodaro means running its processes in more than one container. The default compose layout runs everything in one container: the API, the workers, the orchestrator and the web server. That is fine for up to about 5 active users. For more, run the workers in their own containers. They share one Redis, one database and one storage bucket, and coordinate only through the queues in Redis.
The processes
The image's start script, /app/start.sh, starts these processes side by side, from /app/backend:
| Command | What it does | Load |
|---|---|---|
node dist/server.js | The HTTP API | Low CPU, moderate memory |
node dist/worker.js | The media worker: one job per node, calls the model providers | Waits on the network, runs many jobs at once |
node dist/render-worker.js | The renderer: compositions, in a headless Chrome | Limited by CPU: 1 or 2 per machine |
node dist/orchestrator.js | The workflow orchestrator: runs each workflow's graph | Waits on the network, low CPU |
node dist/pipeline-worker.js | The Story to Video pipeline, which runs only on Nodaro Cloud | Exits at once on self-hosted editions |
The start script also does four things that a container running a single process does not:
- It runs the web server on port
3000in front of the API. - It applies the migrations on the bundled stack.
- It generates
INTERNAL_ORCHESTRATOR_SECRETwhen the variable is unset. - It generates the encryption key on the bundled stack.
Read the script in the image before you split it.
A typical split
- 1 API container, running
server.js. - Several media-worker containers.
VIDEO_WORKER_CONCURRENCY=50, the default, is fine for each. - 1 or 2 render-worker containers, each on its own machine.
- 1 orchestrator container.
The containers never talk to each other directly. They all use the same Redis, Supabase and storage, and Redis is the only coordination point.
What every container must share
| Variable | Why it must match |
|---|---|
INTERNAL_ORCHESTRATOR_SECRET | The orchestrator authenticates to the API with it. The start script generates a new one in each container when it is unset, so set it explicitly, to the same value everywhere. |
NODARO_ENCRYPTION_KEY | Stored provider keys and credentials are encrypted with it. Another key cannot read them. |
RUNTIME_ENV | Names the install. Every container of one install must use the same value. |
EDITION, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, REDIS_URL and the R2_* variables | The same edition, database, queues and storage in every container. |
Concurrency
| Variable | Default | What it limits |
|---|---|---|
MAX_CONCURRENT_NODES_PER_EXECUTION | 6, at most 20 | Nodes that one workflow run may run at once. The server-wide ceiling on parallel nodes. |
VIDEO_WORKER_CONCURRENCY | 50 | Jobs at once in one media worker |
ORCHESTRATOR_CONCURRENCY | 20 | Workflow jobs at once in one orchestrator |
RENDER_WORKER_CONCURRENCY | 2, at most 10 | Renders at once in one render worker. Each is a headless Chrome. |
REMOTION_CONCURRENCY | 2 for 3D scenes; half of the CPU cores for other renders | Browser tabs per render. Keep it low when several 3D jobs run: each WebGL tab adds threads and counts toward the container's process limit. |
FFMPEG_CONCURRENCY | 4, at most 32 | ffmpeg processes at once, across every video and audio editing node |
The compose file does not pass these variables from .env. Add them under environment: of each service that needs them.
Each node of a run may take up to 90 minutes, and a whole run up to 120 minutes. A node that declares its own time budget gets that budget instead, and the run's limit grows by the same amount. Apply EDL is such a node: its budget depends on the edit it renders.
Redis high availability
The job queues support Redis cluster mode. Set REDIS_URL to a cluster endpoint or a Sentinel URL.
Besides the queues, the API keeps small shared caches in Redis, so several API containers do not each repeat the same slow provider work. Today this is the HeyGen avatar and voice catalog, about 4 MB. One container refreshes it under a lock every HEYGEN_CATALOG_REFRESH_HOURS, 24 by default, and the others adopt the new copy within about half a minute. Everything there is a cache: when Redis cannot be reached, each container uses its own memory, and a lost entry is filled again from the provider on the next boot.
Two installs, one database
You can point a second install, such as a staging copy, at the same Supabase project with its own Redis. Then give each install a different RUNTIME_ENV. On Railway, RAILWAY_ENVIRONMENT_NAME already does this.
Each run records the name of the install whose orchestrator claimed it, and each install's clean-up only checks its own runs. Without distinct names, each install searches its own Redis for the other's jobs in its own Redis, cannot find them, and marks healthy runs failed with Execution orphaned. Runs that started before installs recorded their names carry no name; the install named production handles those.
Storage lifecycle
Nodaro never deletes stored media by itself: it only references files by their key. To expire old media, add a lifecycle rule to your bucket, for example after 90 days. Include the video-analysis-tmp/ prefix in the rule: it holds temporary analysis files, and self-hosted installs have no clean-up job for them.
Stopping containers on Railway
On Railway, RAILWAY_DEPLOYMENT_DRAINING_SECONDS sets the time between the stop signal and the forced stop of a replaced container. The media worker drains for that time minus 5 seconds, so a long model call can finish and be saved before its job moves to the new container. Unset, the worker drains for 25 seconds.
Frequently asked questions
Related
Architecture
Configuration
Requirements
Database
Last updated on
Updating
Update a self-hosted Nodaro by pulling a newer image, pin a release tag, prepare for a major version with a backup, and roll back by restoring that backup.
Backups and restore
Back up a self-hosted Nodaro with one command, restore it with another and downgrade safely. Learn what the archive holds and how to protect its key.