Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch

Install

Install self-hosted Nodaro step by step: clone the repository, configure .env, secrets, migrations and object storage, start the stack, sign in and harden it.

This guide installs the Community Edition step by step, with the reasons behind each step. It is the same flow as the Quickstart, plus managed services, secrets, storage and hardening. On the bundled compose stack, most steps need no action: read them to know what happens and what to change later.

1. Clone and configure

git clone https://github.com/nodaroai/app.nodaro.ai.git nodaro
cd nodaro

On the community compose stack, .env is optional. The compose file bundles Supabase, MinIO and Redis with working defaults. Create a .env only to add provider keys, to change the public URL, or to point Nodaro at your own managed services.

To start one, copy the example made for the compose stack:

cp .env.community.example .env

Do not copy .env.example for the compose stack

.env.example is the reference for installs outside the compose stack. Its placeholder values, such as SUPABASE_URL=https://YOUR-PROJECT.supabase.co, replace the compose defaults and break the bundled database.

The values you are most likely to set:

PUBLIC_URL=http://localhost:3000        # your install's public address

# Only with a managed Supabase project instead of the bundled one:
SUPABASE_URL=https://YOUR-PROJECT.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...
SUPABASE_ANON_KEY=eyJ...

# At least one model provider:
KIE_API_KEY=
REPLICATE_API_TOKEN=
ANTHROPIC_API_KEY=
ELEVENLABS_API_KEY=

The compose file passes a fixed list of variables to the app. A variable that is not on that list must be added to the nodaro service in docker-compose.community.yml. Configuration lists every variable and says which ones .env can set.

2. Generate the internal secrets

Nodaro uses two secrets of its own:

VariableWhat it does
INTERNAL_ORCHESTRATOR_SECRETAuthenticates the orchestrator to the API inside a Nodaro container. At least 32 characters.
NODARO_ENCRYPTION_KEYA 64-character hex key that encrypts stored credentials: provider keys pasted on /setup, social network sign-in tokens, and the HTTP credentials users save for Webhook Output. SOCIAL_ENCRYPTION_KEY is the older name and still works.

On the bundled compose stack, skip this step. The container generates both at boot. It saves the encryption key in the app-data volume, at /data/nodaro/encryption-key, and reuses it on every later boot. Back up that volume together with the database.

On your own orchestration, such as a hosting platform or separate containers, set both yourself:

echo "INTERNAL_ORCHESTRATOR_SECRET=$(openssl rand -hex 32)" >> .env
echo "NODARO_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env

Keep NODARO_ENCRYPTION_KEY safe and never change it: everything it encrypted becomes unreadable with another key.

3. Apply the database migrations

On the bundled stack, this is automatic. On boot, the app applies the files in supabase/migrations/ before the API starts. It remembers which files it applied and skips them on the next boot. It refuses to start against a half-migrated database, and its log names the file that failed.

With a managed Supabase project, set RUN_MIGRATIONS_ON_BOOT=false and apply the migrations yourself. See Database.

4. Configure object storage

The bundled MinIO

Nothing to configure. The compose file ships MinIO with these defaults:

VariableDefault
R2_ENDPOINThttp://minio:9000
R2_FORCE_PATH_STYLEtrue
R2_BUCKET_NAMEnodaro-assets
R2_PUBLIC_URLhttp://localhost:3000/storage/nodaro-assets

The app's web server serves the media under /storage/, so the browser and the backend read the same URL. The bucket is created on the first boot, with public read access. The media lives in the minio-data volume.

When you serve the install on a real domain, set R2_PUBLIC_URL=https://<your-domain>/storage/nodaro-assets. Change the MinIO credentials, R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY, before you expose the stack.

Cloudflare R2

Cloudflare R2 is recommended for real deployments, because it has no egress fees.

  1. Create a bucket, for example nodaro-assets, and set R2_BUCKET_NAME to its name.
  2. In the bucket's Settings, enable the public r2.dev address or attach a custom domain. Copy that URL into R2_PUBLIC_URL.
  3. Under Manage R2 API tokens, create a token with Object Read & Write on this bucket. Copy its values into R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY and R2_ACCOUNT_ID.
  4. On the compose stack, also set the endpoint and the addressing style:
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_FORCE_PATH_STYLE=false

An empty value does not clear these two: the compose file falls back to the MinIO defaults for an empty variable. Outside the compose stack, leave both unset, and the endpoint is derived from R2_ACCOUNT_ID.

On boot, the log shows one harmless [storage] failed to create bucket line. R2 tokens cannot create buckets, and yours already exists.

Other S3-compatible stores

For AWS S3, Backblaze B2, DigitalOcean Spaces, Supabase Storage or your own MinIO:

  • Set R2_ENDPOINT to the store's S3 API URL.
  • Set R2_FORCE_PATH_STYLE=true for most self-hosted servers.
  • Set R2_PUBLIC_URL to the bucket's public URL.
  • Set R2_REGION to the store's region, unless the store is Cloudflare R2 or MinIO.

R2_REGION defaults to auto, which is Cloudflare R2's own value and which MinIO ignores. AWS, DigitalOcean Spaces (nyc3, fra1 and more) and a local Supabase (local) reject auto. Every request then fails with an authorization or endpoint error that does not mention the region.

Make the media publicly readable

There are two ways, and most installs need only the first:

  1. A bucket policy. This is the default. With a custom R2_ENDPOINT, the app creates the bucket at boot and gives it an anonymous-read policy. Cloudflare R2 does not need it: its public bucket setting covers it.
  2. A per-object ACL, with STORAGE_OBJECT_ACL. For stores that refuse a bucket policy. DigitalOcean Spaces is the usual case: it refuses a bucket policy from a key scoped to one bucket. Set STORAGE_OBJECT_ACL=public-read, and every object the app writes carries that ACL.

Leave STORAGE_OBJECT_ACL empty unless you need the second way. When it is empty, no ACL header is sent. When it is set on a store whose keys lack the right to set ACLs, every upload fails. Nodaro accepts the standard canned ACLs — private, public-read, public-read-write, authenticated-read, aws-exec-read, bucket-owner-read and bucket-owner-full-control — and refuses any other value at boot.

5. Start the stack

docker compose -f docker-compose.community.yml up

The app image, ghcr.io/nodaroai/nodaro-community, is pulled prebuilt. The first boot downloads about 2.4 GB instead of compiling for 5 to 10 minutes, and later boots take seconds. The logs of Redis and the nodaro service appear side by side. When you see this line, the API is live:

nodaro-1  | server listening on http://0.0.0.0:9000

The web server in the same container serves it on port 3000. Open http://localhost:3000.

latest follows the main branch. To pin a release for a reproducible install, set NODARO_IMAGE in .env, for example NODARO_IMAGE=ghcr.io/nodaroai/nodaro-community:v1.23.0. Updating lists every tag.

To build the image from source instead, run docker compose -f docker-compose.community.yml build. You need this only when you change the code. The public URL, the port, the domain and the Supabase keys are read when the container starts, so the published image serves them after a restart.

6. Sign in for the first time

Sign up in the app with an email address and a password. The sign-in service creates the user, and Nodaro creates the user's profile automatically. On the bundled stack, no confirmation email is sent.

Community Edition users are unrestricted: there is no credit ledger and no admin panel. On the Business edition, promote your first admin next: see First user and admin.

Harden the install before you expose it

The compose defaults are made for local use, and they are public by definition. Before other people can reach the install:

Mint fresh auth keys

node tools/generate-selfhost-keys.mjs >> .env

The script prints SUPABASE_JWT_SECRET, SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY. The two keys are signed with the secret, so all three must come from the same run. A changed anon key needs no rebuild: the container hands it to the browser at runtime, in /config.js.

Set new passwords

Set POSTGRES_PASSWORD and a matching DATABASE_URL:

POSTGRES_PASSWORD=<new-password>
DATABASE_URL=postgres://postgres:<new-password>@db:5432/postgres

Also set fresh MinIO credentials in R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY.

The database aligns its internal role passwords with POSTGRES_PASSWORD only on its first start. If the db-data volume already exists, either delete the volume, which deletes your data, or change the role passwords by hand as supabase_admin.

Serve it over HTTPS

Set PUBLIC_URL to your real https:// address and put a reverse proxy in front of the stack. See Reverse proxy and HTTPS.

Decide who can sign up

The Community Edition is made for a single operator. Anyone who can reach the sign-up page can create an account, and every signed-in user can change the provider keys. On the Business edition, only admins manage provider keys. See Editions and surface profiles.

Apply every change with docker compose -f docker-compose.community.yml up -d, then check /setup.

Frequently asked questions

Last updated on

On this page