Ports & host detection
norsk-ctl binds three classes of port: the daemon’s local API (talked to by the CLI), the proxy front door (what your browser hits), and per-instance ingest ports (SRT, RTMP, etc. that your pipelines listen on). This page is the single source of truth for the defaults, how to change them, and how the installer picks the public host that goes into the TLS certificate.
Defaults
Section titled “Defaults”The proxy port depends on the network mode. From shared/src/derived-settings.ts:
| Network mode | Default proxy port | Notes |
|---|---|---|
docker (default) | 443 | The default for install.sh --server |
hybrid | 443 | Same binding as docker; set --network-mode hybrid at install / init |
TLS is always on; the insecure-HTTP mode was removed (#280).
Plus, regardless of network mode:
| Port | Service | Direction | Notes |
|---|---|---|---|
| 8333 | norsk-ctl daemon API + /mcp | Local-only | CLI and the norsk-ctl mcp stdio bridge talk to this. Do NOT expose to the public internet. The nginx proxy enforces a loopback bypass on /mcp — external /mcp requests are blocked |
| 22 | SSH | Inbound | To run the installer and operate the box. |
| 80 | HTTP→HTTPS redirect + Let’s Encrypt issuance | Inbound | Bound by default for the redirect (a user typing http://<host>/ is bounced to HTTPS) and used by certbot HTTP-01. When 80 belongs to something else, --http-redirect-port <n> moves the listener, or --no-http-redirect drops it — the latter also makes certbot unavailable, since HTTP-01 needs 80. |
| 5001+ (typical) | Media ingest | Inbound | SRT/RTMP/etc. — configured per instance, published from media containers to the host. 5001 is the SRT default (backend/src/docker/constants.ts:62); your pipeline can listen on any port you give it. |
How to change the proxy port
Section titled “How to change the proxy port”The proxy port lives in the proxyPort field of config.yaml. Two ways to set it:
At install or init — --proxy-port N
Section titled “At install or init — --proxy-port N”sudo bash install.sh --server --license /tmp/license.json --ip auto --proxy-port 8443# or, post-install:norsk-ctl init --proxy-port 8443Writes proxyPort: 8443 into config.yaml. The defaults above don’t apply once proxyPort is set.
Edit config.yaml
Section titled “Edit config.yaml”# /etc/norsk-ctl/config.yaml (or ~/.norsk-ctl/config.yaml for --local installs)proxyPort: 8443sudo systemctl restart norsk-ctlBehind another reverse proxy — externalPort
Section titled “Behind another reverse proxy — externalPort”When something else fronts the box — publishing 443 for several services and forwarding one of them here — the port the proxy binds is no longer the port a browser connects to. Set both:
sudo bash install.sh --server --license /tmp/license.json \ --public-host studio.example.com --proxy-port 8443 --external-port 443# or, on a running daemon:norsk-ctl config set --proxy-port 8443 --external-port 443sudo systemctl restart norsk-ctl| Field | Question it answers |
|---|---|
proxyPort | Where do we listen? — so it’s what the fronting proxy forwards to |
externalPort | Where do clients arrive? — so it’s what every advertised URL names |
externalPort defaults to proxyPort, which is correct whenever nothing fronts
the box. Set only proxyPort behind a fronting proxy and the oauth2 sign-in
redirect — plus the base URL baked into each instance at launch — names :8443,
a port the fronting proxy doesn’t publish, so sign-in dead-ends.
On the fronting proxy: forward to https://<box>:8443 preserving Host, allow
WebSocket upgrades, and turn response buffering off (the UI uses Server-Sent
Events; instance screens use WebSockets). Our certificate is self-signed unless
you supplied one, so that hop needs verification disabled or a real certificate
via --cert-source user.
Only a subdomain at the root is supported (https://studio.example.com), not
a path prefix (https://example.com/ctl): the UI bundle, the nginx routes, the
oauth2 callback and the per-instance URL prefixes are all rooted at /.
Public host and --ip auto
Section titled “Public host and --ip auto”The public host is the hostname or IP your clients type into the browser. It serves two roles:
- It’s baked into the self-signed TLS certificate’s Subject Alternative Name (SAN), so the browser doesn’t reject the cert as for-the-wrong-host.
- It’s the prefix the proxy advertises for per-instance URLs (Studio, API, etc.).
You set it at install time. Three options:
--public-host <name> (explicit)
Section titled “--public-host <name> (explicit)”Use the exact string you’ll type into the browser — DNS name or IP:
sudo bash install.sh --server --license /tmp/license.json --public-host norsk.example.comPrefer this when:
- You have a DNS name (it’ll appear correctly in the cert SAN).
- You’re on a private network and the public-IP probe won’t find a usable address.
- You’re behind NAT and the box’s outbound IP differs from what clients reach.
The value must match what you type into the browser exactly, because it prefixes
every advertised instance URL. If clients reach the box on a non-standard
port, include it — --public-host 127.0.0.1:8443. A host carrying an explicit
port is used verbatim; without one, the proxy port is appended. This is what
makes a tunnelled setup work, where clients connect to a forwarded local port
rather than to the box’s own address:
# clients reach the proxy through `ssh -L 8443:localhost:443`norsk-ctl config set --public-host 127.0.0.1:8443--ip auto (autodetect)
Section titled “--ip auto (autodetect)”sudo bash install.sh --server --license /tmp/license.json --ip autoThe installer probes the public IP by curling these endpoints in order (deployment/install.sh:132-138):
https://api.ipify.orghttps://ifconfig.me
Each call has a 5-second timeout. The first one that responds with a non-empty IP wins, and that IP becomes --public-host for init. If both fail, the install aborts with couldn't auto-detect a public IP — pass --ip <host>.
--ip auto is most useful on a public cloud VM where the box has a single public IP and you don’t have a DNS name yet.
Neither (prompt)
Section titled “Neither (prompt)”If you don’t pass either flag and the install is interactive, it prompts: Public host/IP clients use to reach this box (blank = localhost only). Leave blank for a localhost-only install (useful for evaluation on a desktop with no remote clients).
Changing the public host later
Section titled “Changing the public host later”publicHost lives in config.yaml. Editing it requires re-issuing the TLS cert so its SAN matches the new host:
sudo norsk-ctl init --force --public-host new.example.com --cert-source self-signedFor certbot-issued certs, point at the existing letsencrypt path:
sudo norsk-ctl init --force --public-host new.example.com \ --cert-source certbot --domain new.example.com --cert-email ops@example.comPer-instance ingest ports
Section titled “Per-instance ingest ports”Ingest ports aren’t a global config — they’re declared per instance when you launch it (in the workflow YAML, or via the Studio UI). The media container --publishes each declared port to the host, so they need to be open in the host firewall.
To see which ingest ports a running instance is using, open the instance overview in the web UI, or query the daemon API:
curl -sf https://<host>/api/instances/<id> | jq '.media.ingestPorts'(The CLI talks to the daemon via the local :8333 socket; --host <url> lets you point it at a remote one if needed.)
The Studio host port binds loopback
Section titled “The Studio host port binds loopback”Every product publishes a Studio host port — STUDIO_HOST_PORT, the raw Studio editor and API on something like 18000. Docker publishes a port with no bind IP on 0.0.0.0, and inserts its forwarding rules ahead of host firewalls such as ufw, so a ufw deny rule does not protect one. That used to put an unauthenticated control surface on every interface of the box.
So norsk-ctl rewrites the compose it launches to give the studio service’s published ports an explicit 127.0.0.1 bind. Only studio — every other service a product declares keeps publishing exactly as the product wrote it, because those ports carry media (a packager origin a CDN pulls, an SRT listener, an RTMP relay) and ctl cannot tell one from the other without the product saying so.
| Port | Binds | Reach it at |
|---|---|---|
| The proxy front door (443) | every interface | https://<host>/ — unchanged, and the intended way in |
| An instance’s UI, dashboards and Studio | every interface, via the proxy | https://<host>/instance/<id>/ — unchanged; nginx reaches the container over the norsk-net bridge, never the host port |
| Media ingest / egress, and any other service’s published ports | every interface | srt://<host>:5001 etc. — unchanged |
The studio service’s ports (STUDIO_HOST_PORT) | 127.0.0.1 only | http://localhost:18000 on the box itself, or an SSH tunnel: ssh -L 18000:127.0.0.1:18000 <host> |
Three things keep even a studio port on every interface: a UDP mapping, a host side naming a port the product declared in allocatedPorts, and — the escape hatch to reach for — an entry that already states a bind IP. 0.0.0.0:18000:8000, 1.2.3.4:18000:8000 and [::1]:18000:8000 are all left exactly as written, so a product that needs its Studio port on a particular interface says so in its own compose.
The opt-out — --publish-debug-ports
Section titled “The opt-out — --publish-debug-ports”For a host where the surrounding network is trusted and you want the direct port back:
norsk-ctl instance launch-template my-instance --template my-product --publish-debug-portsIt is per launch (relaunch re-applies whatever the launch config says), and it only affects the studio service — everything else was binding every interface either way. The opposite end of the scale is --internal-only, which publishes no host ports at all and leaves the instance reachable only over norsk-net.
The launch log names every port it rebound and the flag that undoes it, so norsk-ctl instance logs (or the daemon journal) is where to look if a port you expected is refusing connections. It also names a Studio port it could not rebind — an entry written as a single compose variable ("${STUDIO_PORTS}") has no shape ctl can safely prefix, so it is left published and reported.