# Scaling

> Scale a self-hosted Nodaro beyond one container. Split the API, media workers, render workers and orchestrator, and tune concurrency, Redis and storage.

Source: https://nodaro.ai/docs/self-hosting/scaling

**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 `3000` in front of the API.
- It applies the migrations on the bundled stack.
- It generates `INTERNAL_ORCHESTRATOR_SECRET` when 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](https://nodaro.ai/docs/nodes/video/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

### How many users can one Nodaro container serve?

The default single-container layout is fine for up to about 5 active users. Beyond that, run the media workers, the render workers and the orchestrator in separate containers.

### How do the Nodaro containers talk to each other?

They do not talk directly. Every container connects to the same Redis, database and storage, and the job queues in Redis are the only coordination point.

### Why are my runs marked Execution orphaned?

Two installs probably share one database but use separate Redis instances without distinct names. Give each install its own RUNTIME_ENV value, and use the same value in every container of one install.

### Does Nodaro delete old media from my bucket?

No. Nodaro references stored media and never deletes it by itself. Add a lifecycle rule to your bucket to expire old files, and include the video-analysis-tmp/ prefix in it.
