NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare

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.

A reverse proxy puts your self-hosted Nodaro on a real domain with HTTPS. The Nodaro container already runs a web server, Caddy, on port 3000, so you add one TLS-terminating proxy in front of that port and tell Nodaro its public address. Both nginx and Caddy work.

What port 3000 serves

The web server inside the container puts everything on one origin:

PathWhat answers
/The editor and the apps, as static files
/v1/*The API, which listens on port 9000 inside the container
/storage/*The media in the bundled MinIO
/supabase/*The bundled sign-in and data API
/config.jsThe runtime settings the browser reads before the app starts

Your proxy only needs to forward every request to port 3000.

Option A: nginx

Use this when you already run nginx or another proxy.

server {
  listen 443 ssl http2;
  server_name nodaro.example.com;
  ssl_certificate     /etc/letsencrypt/live/nodaro.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/nodaro.example.com/privkey.pem;

  client_max_body_size 100M;
  proxy_buffering off;          # important for SSE

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
  • client_max_body_size 100M lets uploads through.
  • proxy_buffering off keeps Server-Sent Events flowing. Nodaro streams text output and run progress with them.

Option B: Caddy on the host

Caddy gets a Let's Encrypt certificate for you:

nodaro.example.com {
  reverse_proxy 127.0.0.1:3000 {
    flush_interval -1
  }
}

Open ports 80 and 443, and point the domain's A or AAAA records at the host. flush_interval -1 keeps Server-Sent Events flowing.

Tell Nodaro its public address

After either option, set these in .env:

PUBLIC_URL=https://nodaro.example.com
CORS_ORIGIN=https://nodaro.example.com
# On the bundled MinIO, serve the media from the same domain:
R2_PUBLIC_URL=https://nodaro.example.com/storage/nodaro-assets

Then apply them:

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

No rebuild is needed. At boot, the container writes the public URL, the browser's sign-in address and the anon key into /config.js, and the browser reads that file before the app starts.

PUBLIC_URL is the address of the install everywhere: in sign-in and OAuth callbacks, in media URLs and in the CORS check. http://localhost:3000 and PUBLIC_URL are always allowed as browser origins.

Serve more than one hostname

  • Extra origins. List them in CORS_ORIGIN, comma-separated, for example CORS_ORIGIN=https://nodaro.example.com,http://192.168.1.20:3000.
  • Streams on the visitor's hostname. When the API shares the app's origin, as in the default container, set PUBLIC_URL_SAME_ORIGIN=true. /config.js then gives the browser the API address / instead of PUBLIC_URL, so streams stay on the hostname the visitor opened. Only the exact value true enables it. Leave it unset when the API really lives on another host.

The compose file does not pass PUBLIC_URL_SAME_ORIGIN from .env. Add it under environment: of the nodaro service. See Configuration.

Move the app's pages to their own hostname

An install can answer on a second hostname for the app's pages. The original hostname keeps the API, sign-in and OAuth, published apps, embeds and shared presentations. Both hostnames point to the same service. FLOW_UI_ORIGIN names the new hostname, with PUBLIC_URL the https origin of the hostname you keep. It works in two steps:

# Step 1, the pages' own hostname: every hostname keeps serving every page.
FLOW_UI_ORIGIN=https://flow.example.com

# Step 2, the move: the hostnames listed here send the app's pages to it.
LEGACY_UI_HOSTS=app.example.com

With FLOW_UI_ORIGIN alone, the pages have their own hostname, but nothing moves:

  • Nothing redirects. Every hostname keeps serving every page. The links Nodaro writes, the addresses people are handed and the API calls pages make stay as they were.
  • Saved browser settings are copied once, on the first visit to the new hostname.
  • One canonical home per public page. Each page that search engines may index names its home with a canonical link, on every hostname that serves it. The home screen, /gallery and /tutorials/... name FLOW_UI_ORIGIN. /login, /signup and published apps (/app/...) name PUBLIC_URL.
  • The start log names the new hostname and says that no hostname redirects to it. A misspelled LEGACY_UI_HOSTS shows there.

With LEGACY_UI_HOSTS too, the move is on:

  • What moves. On a hostname listed in LEGACY_UI_HOSTS, a visit to one of the app's pages is redirected to the same path and query on FLOW_UI_ORIGIN. The pages that move include the home screen, the editor, projects, settings, the gallery, the library, apps and templates. List several hostnames with commas or spaces, in any letter case.
  • What stays. The API, OAuth and sign-in, published apps (/app/...), embeds, shared presentations, every file and unknown paths are never redirected. A page loaded inside a frame is never redirected either.
  • One home for public pages. On the new hostname, a visit to /app/..., /embed/... or /present/... is sent back to PUBLIC_URL.
  • Links. Links that Nodaro writes to the app's pages, and the social-connect pop-ups, name the new hostname. The API, the OAuth issuer and the addresses you hand out stay on PUBLIC_URL: published apps, embeds, shared presentations, webhook URLs and the API examples on the settings page.
  • Redirect status. 302 by default, never cached. LEGACY_UI_REDIRECT_STATUS=301 sends a 301 that the browser keeps for an hour.
  • Canonical links stay as in step 1.

In both steps:

  • Saved browser settings. On the first visit to the new hostname, the browser's saved settings are copied once from the original hostname. Only settings the new hostname does not have yet are written, and never a sign-in session. A setting changed later on the original hostname stays there. The two hostnames must be on the same site, such as app.example.com and flow.example.com: across sites, the copy finds nothing to carry.
  • Strict checks. FLOW_UI_ORIGIN and PUBLIC_URL are two different https origins, with no path or port. On an invalid setup, the API refuses to start and names the setting. That happens when LEGACY_UI_HOSTS is set without FLOW_UI_ORIGIN, or when a value is not an https origin. It also happens when LEGACY_UI_HOSTS names only the new hostname, or holds an entry that is not a bare hostname.
  • Other hostnames. Hostnames that are in neither setting are not affected.

FLOW_PUBLIC_URL is another name for FLOW_UI_ORIGIN. The compose file does not pass these variables from .env, so add them under environment: of the nodaro service and restart. No rebuild is needed.

Change the port

To serve the app on another host port, change the host side of the mapping in docker-compose.community.yml, for example "3001:3000". Then set PUBLIC_URL to match, such as http://localhost:3001.

How client addresses are recorded

The web server in the container accepts X-Forwarded-* headers only from proxies on private addresses: 127.0.0.1/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fd00::/8 and ::1.

  • From a trusted proxy, it reduces X-Forwarded-For to one client address: the rightmost entry that is not itself a trusted proxy. An address that a client wrote into the header is skipped.
  • From a proxy on a public address, it replaces those headers with the values it observed itself.

On an intranet, the same rule skips your users' own addresses when those are private too. A client on the LAN can then choose the address that the API records, for example for rate limits.

The embedded editors on your domain

Edit in NodarCut, or Edit on a video result in an app, opens the hosted video editor at freecut.nodaro.ai, which allows embedding only from http://localhost:3000. On any other origin, such as a domain or a LAN address, the browser refuses to embed it, and the panel explains why. Either open an issue on GitHub with the URL you serve, or run your own editor and set FREECUT_URL. See Configuration.

MCP needs its own hostname

MCP clients must reach the API on port 9000 directly: the web server on port 3000 refuses MCP requests. See MCP.

Serve other apps on the same host

The web server in the container also reads every *.caddy file in /etc/caddy/sites.d/ as part of its one site. To serve another web app beside the editor on the same host, build an image on top of the Nodaro image and add the app's routes there. You do not edit the built-in configuration.

FROM ghcr.io/nodaroai/nodaro-community:latest
COPY --from=<other app's build> /dist /srv/other-app
COPY other-app.caddy /etc/caddy/sites.d/other-app.caddy
# /etc/caddy/sites.d/other-app.caddy
@other_app path /other /other/*
handle @other_app {
	root * /srv/other-app
	encode zstd gzip
	try_files {path} /index.html
	file_server
}
  • Directives, not sites. A file holds directives of the existing site, such as handle or redir, never a site block of its own. Caddy orders handle blocks by their paths, so a route in this directory is answered before the editor's own catch-all route.
  • Paths that do not overlap. Keep away from the editor's paths: /v1/*, /supabase/*, /storage/*, /.well-known/*, /config.js, /assets/* and the editor's own pages. Keep each app's files under its own path too.
  • Exact matches. Match a path exactly and with /*, as in /other /other/*. A bare prefix such as /other* would also take /other-thing.
  • No files is the default. The directory does not need to exist. Without it, the site is unchanged, and Caddy logs one expected warning at start: No files matching import glob pattern.
  • Check the file before you deploy. A file that Caddy cannot parse stops Caddy from starting, and then nothing answers on port 3000. Check the layered image with caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile.

After a sign-in, a path of such an app is loaded in full, so the app answers it instead of the editor. See Single sign-on.

Frequently asked questions

Does the Nodaro container already include a web server?

Yes. The container runs Caddy on port 3000. It serves the editor, forwards /v1 to the API on port 9000, and serves media and the bundled sign-in on the same origin. For HTTPS, put a TLS-terminating proxy in front of port 3000.

What must I change after I put Nodaro behind a domain?

Set PUBLIC_URL and CORS_ORIGIN to your https address, and on the bundled MinIO set R2_PUBLIC_URL to https://your-domain/storage/nodaro-assets. Then run docker compose up -d. No rebuild is needed.

Why does my nginx proxy break streaming output?

nginx buffers responses by default, which holds back Server-Sent Events. Set proxy_buffering off. With Caddy on the host, set flush_interval -1 in the reverse_proxy block.

Can Nodaro answer on more than one hostname?

Yes. List the extra origins in CORS_ORIGIN, comma-separated. If the API shares the app's origin, as in the default container, also set PUBLIC_URL_SAME_ORIGIN=true so streams stay on the hostname the visitor uses.

Last updated on

On this page