Skip to content

Architecture

norsk-ctl is a single binary. When it runs it puts three kinds of process on the host: the daemon itself, an nginx reverse proxy (always — both network modes front every instance through it), and the instance containers — the services each product template defines, plus optional sidecars — for every instance you launch. For the Studio product a template’s services are a Studio + Media pair; that’s the worked example this page uses throughout.

Each instance comes from the product → product template → instance pipeline: launching a product template creates a docker-compose project from the product template’s services, with the runtime concerns layered on by the runner. This page is the runtime topology that pipeline produces.

  • Daemon — the norsk-ctl binary running long-lived. Hosts the HTTP API (:8333, local-only), the web UI, the MCP server (at /mcp), and orchestrates Docker for everything else. CLI, web UI, and MCP host all drive this one daemon.
  • Proxy containernorsk-proxy, nginx on the norsk-net Docker bridge. Terminates TLS, enforces authentication, routes /api/… to the daemon on the host, /instance/<id>/… to the right Studio container, and /products/<name>/… to a registered product’s config UI. Always present — both network modes front Studio with it.
  • Studio + Media per instance — each launched instance is a docker-compose project named from its INSTANCE_NAME, with a studio service (web canvas + gRPC client) and a media service (the media engine). Compose names the containers <id>-studio-1 and <id>-media-1. Both sit on norsk-net in docker mode; the media container moves to the host network in hybrid for low-latency ingest.
  • Sidecars (optional) — extra services you merge into an instance’s compose project. Named <id>-<service>-1 by compose. See Sidecars.

See Network Modes for how these pieces vary between docker and hybrid networking.

Two traffic classes, and the distinction matters. Proxied traffic (browser requests and API calls) flows through nginx on the Docker bridge. Direct traffic (SRT/RTMP media ingest) bypasses nginx and binds straight from <id>-media-1 to the host.

  1. Browser → nginx — nginx publishes a single host port (default 443 across all network modes; see Ports). It’s the only external port the proxy stack listens on (plus port 80 for an HTTP→HTTPS redirect, unless --no-http-redirect was passed).
  2. nginx → backend — requests to /api/, /mcp, and the web UI root are forwarded to host.docker.internal:8333. The MCP loopback bypass means a local norsk-ctl mcp stdio bridge talks directly to :8333 without going through nginx.
  3. nginx → studio — requests to /instance/<id>/… are forwarded to <id>-studio-1:8000 on norsk-net, matched by instance ID in the URL path. WebSocket upgrades pass through.
  4. Studio → media — Studio talks to its paired Media container over gRPC (:6790) and HTTP (:8080) on norsk-net.
  5. Ingest → media — SRT, RTMP and other ingest ports are published directly from <id>-media-1 to the host. They never touch nginx.

Internal container ports (8000, 6790, 6791, 8080) are resolved by container name on norsk-net. Multiple instances can all listen on the same internal ports without conflict — Docker routes by name, not by port.

Ingest ports are different. They are published directly to the host, so two instances binding the same host port will clash. Ingest ports must be globally unique across all running instances.

Each instance launches as a docker-compose project. The product template supplies a deliberately minimal compose.yml (just the studio + media images); the runner layers the runtime concerns on at launch, so the same product template runs correctly under different hosts and network modes. The composed file set is:

product template compose.yml (the instance's services)
runner override (healthchecks, working-dir volume, studio
command + env, license secret)
network-mode overlay (picks docker/hybrid)
sidecar files (optional — one per --sidecars path)

The override and overlays merge over the product template compose, so they fill in (and can override) service definitions. See Products & Templates for what a product template carries.

  • ~/.norsk-ctl/config.yaml — daemon settings. Read once at boot, written by norsk-ctl init and config set. See Configuration.
  • ~/.norsk-ctl/norsk-ctl.db — SQLite, daemon-managed. Holds instance records, stored product templates, and launch configs (so instance restart and workflow switches preserve sidecars and other launch options). Migrated forward-only by the daemon at startup.
  • ~/.norsk-ctl/products.yaml — registered products (the manifest + registration spec for each). Separate from config.yaml. See Products & Templates.
  • <workingDirectory>/ — host-side directory, owned by you. Holds plugins/, studio-save-files/ (workflows), dashboards/. Bind-mounted into Studio.
PortContainerDirectionProxied?ProtocolUnique across instances?Notes
443 (default)norsk-proxyhost → container :9090HTTPSN/A (single proxy)Proxy listen port; override via proxyPort
80norsk-proxyhost → containerHTTPN/AHTTP→HTTPS redirect + certbot HTTP-01; off with --no-http-redirect
8333host (daemon)proxy → hostYesHTTPN/AAPI + web UI + /mcp. Loopback-only; do not expose
8000<id>-studio-1proxy → containerYesHTTP/WSNo — per-containerStudio UI, resolved by container name on norsk-net
6790<id>-media-1studio → mediaNo (internal)gRPCNo — per-containerMedia engine API
6791<id>-media-1container-internalNo (internal)HTTPNo — per-containerHealth check
8080<id>-media-1studio → mediaNo (internal)HTTPNo — per-containerMedia HTTP API
SRT/RTMP/…<id>-media-1host → containerNo (direct)UDP/TCPYes — must be uniqueIngest ports bound directly to host

TLS is mandatory — the insecure HTTP mode was removed. nginx terminates TLS on proxyPort with:

  • TLSv1.3 only, post-quantum cipher group X25519MLKEM768 + TLS_AES_256_GCM_SHA384
  • HTTP/2 enabled

Cert sources: mkcert (local dev), self-signed (auto-generated, IP-friendly), user (paths you supply), certbot (Let’s Encrypt — requires a public DNS name). See Security.

Upstream connections (proxy → daemon, proxy → Studio) stay plain HTTP over norsk-net.