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 nodaroOn 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 .envDo 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:
| Variable | What it does |
|---|---|
INTERNAL_ORCHESTRATOR_SECRET | Authenticates the orchestrator to the API inside a Nodaro container. At least 32 characters. |
NODARO_ENCRYPTION_KEY | A 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)" >> .envKeep 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:
| Variable | Default |
|---|---|
R2_ENDPOINT | http://minio:9000 |
R2_FORCE_PATH_STYLE | true |
R2_BUCKET_NAME | nodaro-assets |
R2_PUBLIC_URL | http://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.
- Create a bucket, for example
nodaro-assets, and setR2_BUCKET_NAMEto its name. - In the bucket's Settings, enable the public
r2.devaddress or attach a custom domain. Copy that URL intoR2_PUBLIC_URL. - 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_KEYandR2_ACCOUNT_ID. - On the compose stack, also set the endpoint and the addressing style:
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_FORCE_PATH_STYLE=falseAn 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_ENDPOINTto the store's S3 API URL. - Set
R2_FORCE_PATH_STYLE=truefor most self-hosted servers. - Set
R2_PUBLIC_URLto the bucket's public URL. - Set
R2_REGIONto 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:
- 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. - 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. SetSTORAGE_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 upThe 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:9000The 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 >> .envThe 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/postgresAlso 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
Related
Quickstart
Configuration
Database
Reverse proxy and HTTPS
First user and admin
Last updated on
Requirements
What a self-hosted Nodaro needs: Docker 24+ with Compose v2, a Supabase database, S3-compatible storage, ports and a provider key or a Nodaro Cloud connection.
Reverse proxy and HTTPS
Serve a self-hosted Nodaro on your own domain over HTTPS, with nginx or Caddy in front of port 3000, then set PUBLIC_URL and CORS_ORIGIN and restart the app.