# Configuration

> Every environment variable of a self-hosted Nodaro, grouped by purpose, with its default, whether .env can set it on the compose stack, and what it changes.

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

This page is the **configuration reference** of a self-hosted Nodaro: every environment variable that operators set, grouped by purpose, with its default. On the bundled compose stack, every variable has a working default, so you set only what you want to change. Variables that apply only to Nodaro Cloud are not listed.

## How to set a variable

On the compose stack, you set a variable in one of two places:

- **In `.env`**, next to `docker-compose.community.yml`. This works for the variables that the compose file passes to the app. The tables below say **Yes** in the **.env** column for them.
- **In the compose file**, for every other variable. Add it under `environment:` of the `nodaro` service:

```yaml
nodaro:
environment:
# ...the variables already listed...
MCP_ENABLED: "true"
MCP_PUBLIC_URL: https://mcp.nodaro.example.com
```

Then apply the change:

```bash
docker compose -f docker-compose.community.yml up -d
```

Nodaro reads its environment at boot. The only exception is the provider keys you paste on `/setup`, which apply at once. Outside the compose stack, set the variables in your platform's environment settings.

**Required outside the compose stack:** `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` and `INTERNAL_ORCHESTRATOR_SECRET`. Without them, the API refuses to start and lists the missing or invalid variables.

## Instance

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `EDITION` | `community` | No | `community`, `business` or `cloud`. The compose file fixes it to `community`. See [Editions and surface profiles](https://nodaro.ai/docs/self-hosting/editions-and-profiles). |
| `PUBLIC_URL` | `http://localhost:3000` | Yes | The install's public origin: sign-in and OAuth callbacks, media URLs and the CORS check. |
| `PUBLIC_URL_SAME_ORIGIN` | unset | No | `true` gives the browser the API address `/` instead of `PUBLIC_URL`, so streams stay on the hostname the visitor opened. For installs that answer on several hostnames. Only the exact value `true` enables it. |
| `CORS_ORIGIN` | empty | Yes | Extra browser origins, comma-separated. `PUBLIC_URL` is always allowed. On these hosts, the SSO landing redirect is relative. |
| `DEFAULT_LOCALE` | empty (the browser's language) | Yes | The language a new visitor starts in. See [Default language](#default-language). |
| `NODARO_SURFACE_PROFILE` | empty (the full interface) | Yes | Narrows the interface of a Business install. See [Surface profiles](https://nodaro.ai/docs/self-hosting/editions-and-profiles#surface-profiles). |
| `NODARO_TUTORIAL_PACKS` | empty (built-in tutorials only) | Yes | Directories of extra tutorial packs, comma-separated. See [Tutorial packs](#tutorial-packs). |
| `RUNTIME_ENV` | `local` | No | Names the install when two installs share one database but not one Redis. See [Scaling](https://nodaro.ai/docs/self-hosting/scaling#two-installs-one-database). On Railway, `RAILWAY_ENVIRONMENT_NAME` sets it. A [webhook endpoint](https://nodaro.ai/docs/developers/api/webhooks) belongs to the install that created it, so renaming `RUNTIME_ENV` hides the endpoints created under the old name. |
| `PORT`, `HOST` | `8000`, `0.0.0.0` | No | Where the API listens. In the image, the API listens on `9000` behind the web server on `3000`. |
| `NODE_ENV` | `development` | No | `production` in every image. |

## Secrets

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `INTERNAL_ORCHESTRATOR_SECRET` | Generated at boot on the compose stack | Yes | Authenticates the orchestrator to the API. At least 32 characters. Required outside the compose stack. |
| `NODARO_ENCRYPTION_KEY` | Generated on the first boot on the compose stack, and kept in the `app-data` volume | Yes | A 64-character hex key that encrypts stored credentials: pasted provider keys, social sign-in tokens, the HTTP credentials saved for Webhook Output and the signing secrets of [webhook endpoints](https://nodaro.ai/docs/developers/api/webhooks). Without it, registering a webhook endpoint answers `503 encryption_key_missing`. |
| `SOCIAL_ENCRYPTION_KEY` | empty | Yes | The older name of `NODARO_ENCRYPTION_KEY`. Either works. |

Generate a secret with `openssl rand -hex 32`. Back up the encryption key with the database: see [Backups and restore](https://nodaro.ai/docs/self-hosting/backups).

## Database and sign-in

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `SUPABASE_URL` | `http://localhost:3000/supabase` on the compose stack | Yes | The Supabase URL the API uses. Required outside the compose stack. |
| `SUPABASE_SERVICE_ROLE_KEY` | A public default key on the compose stack | Yes | The service-role key the API uses. Required outside the compose stack. |
| `SUPABASE_ANON_KEY` | A public default key on the compose stack | Yes | The anon key handed to the browser and the sign-in service. |
| `FRONTEND_SUPABASE_URL` | `PUBLIC_URL/supabase` on the bundled stack | Yes | The Supabase URL the browser uses, written into `/config.js` at boot. Set it with a managed Supabase project. |
| `SUPABASE_JWT_SECRET` | A public default secret | Yes | The secret that signs the anon and service-role keys on the bundled stack. Mint all three with `node tools/generate-selfhost-keys.mjs`. |
| `POSTGRES_PASSWORD` | `nodaro-postgres-pw` | Yes | The bundled database's password. The internal role passwords follow it only on the database's first start. |
| `DATABASE_URL` | `postgres://postgres:nodaro-postgres-pw@db:5432/postgres` | Yes | A direct Postgres URL, used only to apply migrations at boot. Keep its password in step with `POSTGRES_PASSWORD`. |
| `RUN_MIGRATIONS_ON_BOOT` | `false`; `true` on the compose stack | Yes | Applies `supabase/migrations/` before the API starts. Set `false` with a managed Supabase project. |
| `SUPABASE_MANAGED_PROXY` | unset | No | `true` serves a managed Supabase project through the app's own origin. See [Database](https://nodaro.ai/docs/self-hosting/database#serve-a-managed-project-through-your-own-origin). |
| `SUPABASE_PROXY_UPSTREAM` | unset | No | The managed project's origin, without a path, for the proxy above. |

## Queues

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `REDIS_URL` | `redis://localhost:6379`; `redis://redis:6379` on the compose stack | No | The Redis for queues and shared caches. A cluster or Sentinel URL also works. |

## Storage

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `R2_ENDPOINT` | `http://minio:9000` on the compose stack | Yes | The S3 API URL of the store. On the compose stack, an empty value falls back to MinIO. |
| `R2_FORCE_PATH_STYLE` | `true` on the compose stack | Yes | Path-style addressing, which most self-hosted stores need. |
| `R2_ACCOUNT_ID` | empty | Yes | The Cloudflare account ID. Outside the compose stack, the R2 endpoint is derived from it when `R2_ENDPOINT` is unset. |
| `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY` | The bundled MinIO's credentials | Yes | The storage credentials. On the compose stack, they are also the MinIO root credentials. |
| `R2_BUCKET_NAME` | `nodaro-assets` on the compose stack | Yes | The bucket. Outside the compose stack the default is `scenenode-assets`, so always set it. |
| `R2_PUBLIC_URL` | `http://localhost:3000/storage/nodaro-assets` on the compose stack | Yes | The public URL of the bucket. |
| `R2_REGION` | `auto` | Yes | The S3 region. Set a real one for AWS, DigitalOcean Spaces (`nyc3` and more) and a local Supabase (`local`). |
| `STORAGE_OBJECT_ACL` | empty (no ACL header) | Yes | A canned ACL on every uploaded object, for stores that refuse a bucket policy. See [Install](https://nodaro.ai/docs/self-hosting/install#make-the-media-publicly-readable). |
| `R2_PUBLIC_FALLBACK_DOMAIN` | empty | No | A second public host for media, for example the raw `r2.dev` address next to a CDN domain. |
| `R2_SHARED_WITH_RELAY_TARGET` | `false` | No | `true` only when `R2_PUBLIC_URL` names the same bucket that your Nodaro Cloud host writes to. See [Connect to Nodaro Cloud](https://nodaro.ai/docs/self-hosting/cloud-connect#configuration-reference). |

## Model providers

Each of these keys can also be pasted on `/setup`. A key set in the environment wins. See [Provider keys](https://nodaro.ai/docs/self-hosting/provider-keys).

| Variable | Default | .env | What it powers |
| --- | --- | --- | --- |
| `NODARO_API_KEY` | empty | Yes | Nodaro Cloud as a provider, with a personal API token. |
| `KIE_API_KEY` | empty | Yes | The broadest coverage of media and text models. |
| `KIE_API_BASE_URL` | The provider's own API host | Yes | Sends that provider's traffic through your own proxy. It also moves the Claude and Gemini text traffic that runs on `KIE_API_KEY`. |
| `REPLICATE_API_TOKEN` | empty | Yes | An alternative catalog, including the Flux 2 models. |
| `ANTHROPIC_API_KEY` | empty | Yes | Claude, directly, for the text nodes. |
| `GEMINI_API_KEY` | empty | Yes | Gemini, directly. See [Gemini models](https://nodaro.ai/docs/self-hosting/provider-keys#gemini-models). |
| `ELEVENLABS_API_KEY` | empty | Yes | Speech, voices and dubbing. |
| `ELEVENLABS_BASE_URL` | `https://api.elevenlabs.io` | Yes | Sends ElevenLabs traffic through your own proxy. |
| `FAL_KEY` | empty | Yes | Models such as Sync Lipsync v3. |
| `HEYGEN_API_KEY` | empty | Yes | AI Avatar and Cinematic Avatar. |
| `BEEBLE_API_KEY` | empty | Yes | Relight & Switch. |
| `APIFY_API_TOKEN` | empty | Yes | Web Scrape and Site Capture. |
| `SITE_CAPTURE_ENABLED` | on | No | `false` turns off the Site Capture node, `POST /v1/site-capture` and the `capture_site` MCP tool on this install. |
| `HEYGEN_CATALOG_REFRESH_HOURS` | `24` | No | How often the shared list of HeyGen avatars and voices is refreshed. |

## Nodaro Cloud connection

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `NODARO_CLOUD_URL` | `https://app.nodaro.ai` | Yes | The Nodaro Cloud host that **Connect nodaro.ai** registers with, and where calls made with `NODARO_API_KEY` go. Change it only when your install belongs to a dedicated Nodaro deployment. Read at boot. |

`NODARO_API_KEY` and `R2_SHARED_WITH_RELAY_TARGET` also belong to the connection. See [Connect to Nodaro Cloud](https://nodaro.ai/docs/self-hosting/cloud-connect).

## Workers and performance

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `MAX_CONCURRENT_NODES_PER_EXECUTION` | `6`, at most `20` | No | How many nodes one workflow run may run at once. |
| `VIDEO_WORKER_CONCURRENCY` | `50` | No | Parallel jobs in the media worker, which mostly waits on providers. |
| `ORCHESTRATOR_CONCURRENCY` | `20` | No | Parallel jobs in the orchestrator. |
| `RENDER_WORKER_CONCURRENCY` | `2`, at most `10` | No | Parallel renders. Each render runs a headless Chrome. |
| `REMOTION_CONCURRENCY` | `2` for 3D scenes; half of the CPU cores for other renders | No | Browser tabs per render. A value you set applies to both. 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` | No | ffmpeg processes at once, across every video and audio editing node. |
| `RAILWAY_DEPLOYMENT_DRAINING_SECONDS` | unset | No | On Railway only: the time between the stop signal and the forced stop of a replaced container. The media worker drains for this long minus 5 seconds. Unset, it drains for 25 seconds. |

Read [Scaling](https://nodaro.ai/docs/self-hosting/scaling) before you raise these.

## Editors

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `FREECUT_URL` | empty (the hosted editor) | Yes | The video editor behind **Edit in NodarCut** on the canvas and **Edit** on a video result in an app. `off` turns the editor off: the buttons stay, but no editor opens. |
| `AUDIOMASS_URL` | empty (no audio editor) | Yes | The audio editor behind **Edit** on an audio result in an app. `off` behaves like empty. |

See [Video and audio editors](#video-and-audio-editors).

## Updates

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `NODARO_IMAGE` | `ghcr.io/nodaroai/nodaro-community:latest` | Yes | The app image the compose file runs. Pin a release tag here. |
| `NODARO_UPDATE_CHECK` | on | No | `off` stops the daily update check: no request leaves the install. For air-gapped installs. |
| `NODARO_UPDATE_CHECK_TOKEN` | empty (anonymous) | No | A GitHub token for the update check, with no scopes. |

See [Updating](https://nodaro.ai/docs/self-hosting/updating#which-version-you-run).

## MCP

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `MCP_ENABLED` | off | No | `true` or `1` serves the MCP endpoint. |
| `MCP_PUBLIC_URL` | empty (the Nodaro Cloud MCP host) | No | The public address of your MCP host. |
| `MCP_DYNAMIC_REGISTRATION` | `allowlist` | No | Dynamic client registration for MCP clients: `allowlist`, `open` or `off`. |
| `MCP_DCR_ALLOWLIST` | 14 known clients | No | The client names that may register in `allowlist` mode. |

See [MCP](https://nodaro.ai/docs/self-hosting/mcp).

## Sign-in and roles

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `EXTERNAL_SSO_PROVIDERS` | empty (SSO off) | No | Trusted identity providers, as inline JSON or `@/path/to/file.json`. A malformed value stops the boot. |
| `EXTERNAL_SSO_LINK_EXISTING` | `false` | No | Whether single sign-on may link to an existing local account. |
| `PLATFORM_OWNER_EMAIL` | empty | No | Business: the owner account, whose role no other admin can change. |

See [Single sign-on](https://nodaro.ai/docs/self-hosting/sso) and [First user and admin](https://nodaro.ai/docs/self-hosting/first-admin).

## Integrations

| Variable | Default | .env | What it does |
| --- | --- | --- | --- |
| `FIGMA_PLUGIN_OAUTH_CLIENT_ID` | empty (plugin connection off) | No | The client ID of the developer app that the Figma plugin connects through. |
| Social network variables | empty | No | See [Social network apps](#social-network-apps). |

For the Figma plugin, register a developer app on your install with `<PUBLIC_URL>/v1/oauth/plugin/callback` as a redirect URI and the scopes `jobs:read`, `assets:read`, `assets:write` and `credits:read`. Then set its client ID here. When the variable is empty, the plugin connection routes answer `503` and nothing else changes. See [OAuth apps](https://nodaro.ai/docs/developers/oauth).

## Build-time settings

These are fixed when the image is built. Change them in the compose file's `build: args:` and rebuild.

| Build argument | Default | What it does |
| --- | --- | --- |
| `VITE_EDITION` | `community` in the compose file | The edition the editor shows. The build refuses an empty or unknown value. |
| `VITE_STUDIO_URL` | `https://studio.nodaro.ai` | The address for **Open in Studio** links. |
| `VITE_PERSON_URL` | `https://person.nodaro.ai` | The address for the **Open Person** card on the home screen. |

## Video and audio editors

**Edit in NodarCut** on a video result on the canvas, and **Edit** on a video result in an [app](https://nodaro.ai/docs/concepts/apps), open the NodarCut video editor. **Edit** on an audio result in an app opens the AudioMass editor. Your media does not go to the editor's server: the app reads the file from your own storage and hands it to the editor inside the browser. The editing and the export happen there.

- **Video.** With `FREECUT_URL` empty, the editor loads from `freecut.nodaro.ai`, which allows embedding only from `http://localhost:3000`. On another origin, such as a LAN address or a domain, the browser refuses it and the panel explains why. Open an issue on GitHub with the URL you serve to have it allowed, or run your own [FreeCut](https://github.com/nodaroai/freecut). FreeCut is public, MIT-licensed and ships a Dockerfile. Then set `FREECUT_URL=https://freecut.example.internal`.
- **Audio.** There is no public hosted AudioMass, so until you configure it, **Edit** on an audio result opens no editor. On the Present tab it does nothing, and outside the editor it shows a panel that explains what to set. Run Nodaro's fork, [AudioMass](https://github.com/nodaroai/audiomass), which supports the in-browser handoff that the original AudioMass lacks. Then set `AUDIOMASS_URL=https://audiomass.example.internal`.

Restart the stack after a change. No rebuild is needed.

## Model maker logos
`GET /v1/models` names the maker of each model and can carry the maker's logo as `logoUrl`, for apps that draw a model picker. A company's logo is its trademark, so no logo ships with Nodaro. An install publishes only the logos it has the right to show. Every other maker's `logoUrl` is `null`, and apps then show the maker's name. An install that publishes no logo needs no setup.

To publish a maker's logo:

1. **Upload the file** to the bucket behind `R2_PUBLIC_URL`, at `brand/makers/<maker id>.svg`, with the content type `image/svg+xml`. Use the company's official logo as an SVG without scripts. The maker ids are the `maker.id` values that `GET /v1/models` returns.
2. **Declare the maker ids** in the instance setting `model_maker_logos`, a list such as `["google", "openai"]`. On a Business install, the platform operator sets it with `PUT /v1/admin/settings/model_maker_logos` and the body `{"value": ["google", "openai"]}`. On the Community Edition, which has no admin panel, write the same instance setting directly in the database.

- **The server checks the file first.** A declared maker gets a `logoUrl` only once its file is in the bucket as an SVG, so the two steps can come in either order. A missing or wrongly typed file gives `null`, never a broken address.
- **Never open the address in a browser to test an upload.** The CDN, and Caddy on the bundled stack, keep a "not found" answer for a year. Check the file with the storage console or a `HEAD` request, such as `curl -I`.
- **Timing.** A new logo appears within about ten minutes. To replace a logo, upload the new file over the old one: the address changes with the file. To withdraw one, remove its id from the setting or delete the file.
- **Nodaro never deletes these files.** Remove a logo from the storage console when you mean to.

## Default language

`DEFAULT_LOCALE` sets the language that a new visitor starts in. The values are `he`, `ar`, `de`, `fr`, `es`, `hi`, `ja`, `ko`, `pt-BR`, `ru`, `zh-CN` and `en`. A user's own saved choice always wins. An empty, blank or unknown value falls back to the visitor's browser language. Restart to apply. See [Languages](https://nodaro.ai/docs/concepts/languages).

## Tutorial packs

A self-hosted install can add its own tutorials to the built-in set, without a rebuild. Each pack is a directory:

- **`manifest.json`** — the pack's `name` and the `categories` its tutorials use, each with a `slug`, a `name`, and an optional `sortOrder` and `description`. Optional keys: `locale`, `version`, `forbiddenPromptTerms`, and `creatorDisplayName`, the author name shown on every card of the pack.
- **One `*.json` file per tutorial** — a workflow snapshot with its nodes, its connections and its results already made, so a clone opens showing the finished run. Optional card data: `estimatedCredits`, the credits a run costs, and `estimatedPerMinuteCredits` for a tutorial whose price follows the length of the recording it is given. Such a tutorial sets `estimatedCredits` to the fixed part and `estimatedPerMinuteCredits` to the credits per minute of the recording. Also optional: `nodeTypesUsed` and `providersUsed` for the chips on the card, a per-tutorial `creatorDisplayName`, and `listedIn`. `listedIn` is `["tutorial"]` for the Tutorials tab, the default, or `["marketplace"]` for the template marketplace. It applies only on the first seed.

Every media file a tutorial uses must be a public `https://` URL. Each tutorial's category must be declared in the manifest, and it is created on the first boot.

Mount the pack directories into the container read-only, and list their paths inside the container:

```yaml
nodaro:
volumes:
      - ./tutorials/acme:/data/tutorials/acme:ro
```

```bash
NODARO_TUTORIAL_PACKS=/data/tutorials/acme
```

Packs only add tutorials; they never change the built-in ones. A pack that fails validation is skipped whole and logged. Validation fails on invalid JSON, an undeclared category, a media URL that is not public, or a slug that collides with an existing tutorial. Restart the container to load a changed pack.

## Social network apps

Publishing to a social network works per network. A network becomes available once its OAuth app credentials are set. Without them, the network still appears in **Settings › Integrations** as unavailable, with the names of the missing variables.

| Network | Variables |
| --- | --- |
| Instagram | `META_APP_ID`, `META_APP_SECRET`. Optional: `META_INSTAGRAM_CONFIG_ID` for Facebook Login for Business. |
| Instagram, without a Facebook Page | `INSTAGRAM_APP_ID`, `INSTAGRAM_APP_SECRET`. Meta issues these apart from the Facebook app. Tokens refresh by themselves, about every 60 days. |
| Facebook | `META_APP_ID`, `META_APP_SECRET`. Optional: `META_FACEBOOK_CONFIG_ID`. |
| TikTok | `TIKTOK_CLIENT_KEY`, `TIKTOK_CLIENT_SECRET` |
| YouTube | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` |
| LinkedIn | `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET` |
| X | `X_CLIENT_ID`, `X_CLIENT_SECRET` |
| Reddit | `REDDIT_CLIENT_ID`, `REDDIT_CLIENT_SECRET` |
| Pinterest | `PINTEREST_CLIENT_ID`, `PINTEREST_CLIENT_SECRET` |
| Discord | `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`, `DISCORD_BOT_TOKEN` |
| Twitch | `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET` |
| Threads | `THREADS_APP_ID`, `THREADS_APP_SECRET` |
| Mastodon | `MASTODON_CLIENT_ID`, `MASTODON_CLIENT_SECRET`. Optional: `MASTODON_URL`, default `mastodon.social`. |
| Telegram | None: users paste their own bot token. |
| Bluesky | None: users connect with a handle and an app password. |
| Dev.to, Hashnode, Medium | None: users connect with their own API key or token. |
| WordPress | None: users connect with the site URL and an application password. |
| Lemmy | None: users connect with an instance, a login and a community. |

Every OAuth app must allow the redirect URI `https://<your-domain>/v1/social/callback/<network>`. When a Facebook or Instagram login manages more than one Page or account, the connect window asks the user which one to connect. See [Publishing to social networks](https://nodaro.ai/docs/guides/publishing-to-social).

## Frequently asked questions

### Why does a variable in my .env file have no effect?

The compose file passes only the variables listed under the nodaro service's environment block to the app. Add any other variable to that block in docker-compose.community.yml, then run docker compose up -d.

### Which environment variables are required?

None on the bundled compose stack, which sets or generates them. Outside it, the API refuses to start without SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY and an INTERNAL_ORCHESTRATOR_SECRET of at least 32 characters.

### Do I need to restart after I change a variable?

Yes. Nodaro reads its environment at boot, so run docker compose up -d to recreate the container with the new values. Provider keys pasted on /setup are the exception and apply without a restart.

### How do I change the default language of my install?

Set DEFAULT_LOCALE, for example DEFAULT_LOCALE=de, and restart. A new visitor starts in that language. A user's own saved choice always wins, and an unknown value falls back to the browser's language.

### How do I add my own tutorials to a self-hosted install?

Put each tutorial pack in a directory with a manifest.json and one JSON file per tutorial, mount it into the container, and list the directories in NODARO_TUTORIAL_PACKS. Restart to load them.
