# 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.

Source: https://nodaro.ai/docs/self-hosting/reverse-proxy

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.

```nginx
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:

```text
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`:

```bash
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:

```bash
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](https://nodaro.ai/docs/self-hosting/configuration#how-to-set-a-variable).

## 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

The **Edit video** action 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](https://nodaro.ai/docs/self-hosting/configuration#video-and-audio-editors).

## 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](https://nodaro.ai/docs/self-hosting/mcp).

## 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.
