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:
| Path | What 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.js | The 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 100Mlets uploads through.proxy_buffering offkeeps 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-assetsThen apply them:
docker compose -f docker-compose.community.yml up -dNo 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 exampleCORS_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.jsthen gives the browser the API address/instead ofPUBLIC_URL, so streams stay on the hostname the visitor opened. Only the exact valuetrueenables 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.comWith 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,
/galleryand/tutorials/...nameFLOW_UI_ORIGIN./login,/signupand published apps (/app/...) namePUBLIC_URL. - The start log names the new hostname and says that no hostname redirects to it. A misspelled
LEGACY_UI_HOSTSshows 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 onFLOW_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 toPUBLIC_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.
302by default, never cached.LEGACY_UI_REDIRECT_STATUS=301sends a301that 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.comandflow.example.com: across sites, the copy finds nothing to carry. - Strict checks.
FLOW_UI_ORIGINandPUBLIC_URLare two different https origins, with no path or port. On an invalid setup, the API refuses to start and names the setting. That happens whenLEGACY_UI_HOSTSis set withoutFLOW_UI_ORIGIN, or when a value is not an https origin. It also happens whenLEGACY_UI_HOSTSnames 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-Forto 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
handleorredir, never a site block of its own. Caddy ordershandleblocks 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 withcaddy 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.
Related
Install
Configuration
MCP on a self-hosted install
Troubleshooting
Last updated on
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.
First user and admin
Sign up the first user of a self-hosted Nodaro and promote an admin on the Business edition with one SQL statement, with or without a Supabase dashboard.