Remote Access

By default SkyView is LAN-only — once you leave home, the mobile app can no longer reach your server. This chapter covers three ways to expose SkyView safely: public direct access (recommended), mesh VPN, and reverse proxy, and which ports each one needs. Public direct access takes one domain and a single router rule; the certificate is requested automatically from the web UI.

Read this first

There is exactly one hard rule for exposing SkyView: HTTPS plus a strong password. SkyView's public direct access is "one TCP port + TLS + login", not your cameras sitting naked on the internet. But never forward your cameras' own RTSP / ONVIF ports, nor SkyView's internal streaming ports (24214 / 23880) — those have no login layer in front of them, and forwarding them hands your cameras to every scanner on the internet.

Which one should you use

MethodWhat you needRouter workLive latencyBest for
Public direct accessA domain (a free subdomain works); the certificate is issued automaticallyIPv4: one TCP forward rule. IPv6: just allow inboundWebRTC, under 0.5 sConnections with a public IP or IPv6 (recommended)
Mesh VPNThe same mesh app installed on every device you useNothing at allWebRTC, sub-secondCarrier-grade NAT with no IPv6, or if you refuse to open any port
Reverse proxy + domainAn nginx / Caddy / Lucky instance you already runForward whichever port the proxy usesHLS, about 2.5 s (WebRTC once you add TCP pass-through)People already running a proxy who want everything on 443

They are not mutually exclusive — run several at once if you like. If you are unsure, work down this list: check whether your ISP gives you IPv6 (if so, public direct access needs no port forwarding at all); no IPv6 but a public IPv4 address still means public direct access; neither (carrier-grade NAT) means mesh VPN.

SkyView folds the web UI, the app API and the live video onto a single TCP port: the same port carries HTTPS and the WebRTC media channel, and the server tells them apart automatically. That means one router rule — no separate UDP forward for WebRTC.

What this fixes

Remote live view used to fall back to HLS (about 2.5 s of latency): WebRTC needed extra forwarded ports *and* a genuinely reachable address advertised by the server, which home connections rarely satisfy at the same time, so the handshake timed out and downgraded. Now a single port covers everything and remote viewing is sub-second — which is the whole difference for "who is at the door right now".

Step 1: domain and certificate (issued from the web UI)

Public direct access requires HTTPS: mobile apps reject untrusted self-signed certificates, so you need your own domain and a real certificate. Go to Settings → General → HTTPS certificate, enter the domain and your DNS provider's API key; SkyView then requests a Let's Encrypt certificate and renews it automatically before expiry.

  • Port 80 stays closed. Validation is DNS-01 (a temporary TXT record under your domain), so it works even where inbound 80 is blocked by the ISP.
  • Supported DNS providers: Alibaba Cloud, Tencent Cloud DNSPod, Huawei Cloud, Cloudflare, AWS Route 53, Porkbun, deSEC, DuckDNS.
  • No domain of your own? Still fine. deSEC and DuckDNS hand out free subdomains you can use as your domain.
  • Already have a certificate? Drop fullchain.pem and privkey.pem into data/certs/ — SkyView will use them and never overwrite your files.
  • Before issuing, run the self-test: it really writes a probe record and deletes it again to prove your API key and permissions work, and it does not consume any Let's Encrypt issuance quota.

The domain has to be yours

SkyView does not host domains for you — no built-in DDNS, no official subdomains. The domain and its DNS stay in your name; we only automate the certificate, exactly like certbot or acme.sh. If you have no domain, deSEC and DuckDNS (above) are free.

Step 2: pick a public port

On residential lines, think twice before using 443

In mainland China, inbound 80 and 443 are widely blocked on residential broadband (to stop home lines from hosting unregistered websites). Copying a tutorial that says "use 443" produces the worst possible failure mode: the router rule is perfectly correct, the connection simply never arrives, and nothing anywhere reports an error. Use a high port there — the default 23443 is a good pick. Cloud servers and most other regions have no such restriction, so 443 is fine and saves typing the port in the URL.

Enter the public port in the same panel and save — it takes effect immediately. The backend reconfigures itself and then confirms the port really reached the listening state; no container restart, no compose edits. If the port is taken by another program or collides with an internal one, the save is rejected on the spot instead of silently failing later.

Step 3: the router

  • IPv4 (public address): add one port-forward rule sending public 23443/tcp to 23443 on the SkyView host. That is the only rule — you do not need to forward 23515 or anything else.
  • IPv6: there is no NAT, so nothing to forward. Just allow inbound connections to 23443/tcp in your router's IPv6 firewall.
  • DNS: an A record for public IPv4, an AAAA record for IPv6. Home addresses change, so point a DDNS client at the record to keep it current.

Step 4: verify

Open https://<your-domain>:23443/, sign in and go to Live. The picture should appear within a second, over WebRTC.

The same panel has a public reachability self-test: it checks the address your domain resolves to, whether the port is open, and whether the certificate is right, then tells you which step is failing — no packet capture required.

What if your ISP gives you no public IP

If the self-test reports that your public address falls inside a carrier-grade NAT (CGNAT) range, port forwarding cannot work no matter how you configure it — this is not a misconfiguration. Two ways out: use IPv6 (widely available now, and it needs no forwarding at all), or switch to mesh VPN below. You can also ask your ISP for a public address; many will enable it for free.

Method 2: mesh VPN (no open ports at all)

Mesh tools (Tailscale / WireGuard / ZeroTier, etc.) build an encrypted tunnel between your phone and your home server, so the phone accesses SkyView via the internal address just like at home — no need to open any ports on the router, and the server isn't exposed to the internet at all.

  1. 1

    Install the mesh client on the server

    Install Tailscale on the machine running SkyView (or a soft router on the same LAN), then tailscale up to log in. WireGuard / ZeroTier are similar.

    bash
    curl -fsSL https://tailscale.com/install.sh | sh
    sudo tailscale up
  2. 2

    Install the same app on your phone and log into the same account

    Install the corresponding Tailscale / ZeroTier app on the phone and log into the same account as the server; both ends are now on the same virtual LAN.

  3. 3

    Enter the mesh address in the app

    Set the SkyView app's server address to the mesh-assigned IP — Tailscale uses 100.x.x.x, ZeroTier uses 10.x.x.x — the port is still :23406, e.g. http://100.x.x.x:23406.

  4. 4

    Done

    While away, the phone can access it even on 4G/5G, just like at home. No public IP, no domain, no HTTPS certificate needed.

The trade-off

No open ports means zero public attack surface — scanners cannot even see you — and it does not depend on having a public IP, so it works behind carrier-grade NAT. Inside the mesh your phone counts as being on the LAN, so live view still uses the low-latency WebRTC path. The cost is that every device you want to use must install and sign in to the mesh app, which makes ad-hoc sharing with family clumsier than handing over a URL.

Method 3: reverse proxy + domain

If you have a public IP + a domain and want to access it at an address like https://cam.example.com (convenient to send to family who can't easily install the mesh app), you can put a layer of nginx / Caddy in front of SkyView for HTTPS termination. The SkyView container already includes a layer of nginx internally, so the outer proxy only needs to forward all traffic to :23406.

What live video does behind a proxy

An ordinary HTTP-layer reverse proxy forwards the web UI and the API, and live view runs over HLS (about 2.5 s) — fully functional. For sub-second WebRTC, have the proxy pass a port straight through at the TCP layer (nginx stream, Caddy's layer4 plugin) to SkyView's public port. The two coexist happily: the web UI on 443 through the proxy, live video through the pass-through port.

nginx config

Save the content below in full as /etc/nginx/sites-available/skyview.conf, replace <your-domain>, then ln -s it into sites-enabled/ — one file does it, no separate snippet needed. The proxy_set_header blocks in the two location / are identical: nginx's proxy_set_header overrides rather than inherits, so each location must carry its own copy — just copy it verbatim.

nginx
# WebSocket Upgrade passthrough — must be in the http {} context
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name <your-domain>;
    # certbot --nginx will rewrite this block to a 301 redirect to https
    location / {
        proxy_pass http://127.0.0.1:23406;
        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-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;
        proxy_set_header X-Forwarded-Proto $scheme;        # ★ Omit it and HTTPS deployments hit a login loop
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
        proxy_buffering       off;
        proxy_connect_timeout 60s;
        proxy_send_timeout    1d;
        proxy_read_timeout    1d;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name <your-domain>;

    ssl_certificate     /etc/letsencrypt/live/<your-domain>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<your-domain>/privkey.pem;

    # Recording exports and bulk face-library imports can be hundreds of MB; long JWT cookies are large, prevent 414
    client_max_body_size        200m;
    client_header_buffer_size   4k;
    large_client_header_buffers 8 16k;

    location / {
        proxy_pass http://127.0.0.1:23406;
        proxy_http_version 1.1;
        # ↓ Identical to the :80 location above, copy line for line (proxy_set_header doesn't inherit)
        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-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
        proxy_buffering       off;
        proxy_connect_timeout 60s;
        proxy_send_timeout    1d;
        proxy_read_timeout    1d;
    }
}
/etc/nginx/sites-available/skyview.conf

X-Forwarded-Proto / X-Forwarded-Host must be passed + included in every location

The container's nginx listens on HTTP only; it relies on X-Forwarded-Proto to know the client's real scheme and X-Forwarded-Host to know the external domain. Omitting X-Forwarded-Proto → on HTTPS deployments you're kicked back to the login page right after logging in, and the live view is blocked by the browser as Mixed Content; omitting X-Forwarded-Host → recording playback / download / export links are built with wrong addresses and the client can't open them. The nginx template above and Caddy (which passes these two headers by default) both cover this — just copy it. You don't need to set X-Forwarded-Port by hand; the container's nginx handles both "outer reverse proxy / bare-IP direct" scenarios automatically. Also, nginx's proxy_set_header overrides rather than appends: if a location writes any single one, the upper-level set_headers all stop applying — so each location must include the complete snippet.

Obtain a certificate (the machine must be reachable from the internet on :80):

bash
certbot --nginx -d <your-domain> -m <your-email> --agree-tos --no-eff-email --redirect

Caddy config (simpler)

Caddy auto-issues / renews Let's Encrypt certificates without certbot, and the config is much shorter. /etc/caddy/Caddyfile:

caddyfile
<your-domain> {
    reverse_proxy 127.0.0.1:23406 {
        # Caddy passes X-Forwarded-{For,Proto,Host} by default
        # For long-lived connections (talkback WS / event SSE / live stream), raise flush + timeouts
        flush_interval -1
        transport http {
            read_timeout  24h
            write_timeout 24h
        }
    }
    request_body {
        max_size 200MB
    }
}
/etc/caddy/Caddyfile

Port cheat sheet

PortProtocolPurposeOpen to the internet?
23443TCPPublic direct access: web UI + API + live video (HTTPS, port configurable)Yes — the only port this method needs
23406TCPLAN entry point (plain HTTP)No — use 23443 above for remote access
23515UDP + TCPLive media stream (LAN / mesh / IPv6 direct)No
23880TCPRTSP for external players on the LANNo
24214TCPInternal streaming serviceNever

Never forward the internal ports

24214 (internal streaming service) and 23880 (RTSP) have no login layer — stream authorisation happens at the 23406 / 23443 entry point. Exposing them bypasses authentication entirely and publishes your camera feeds. The same goes for forwarding 23406 as-is: that is plain HTTP, so passwords cross the internet in the clear, and browsers plus Android 9+ refuse camera permissions on insecure pages. For remote access use public direct access (23443, HTTPS) above.

How to verify after configuring

bash
# 1. HTTP→HTTPS redirect (reverse-proxy scenario)
curl -sSI http://<your-domain>/healthz | head -1      # expect 301

# 2. Health check
curl -sS https://<your-domain>/healthz                 # expect {"code":0,...}

# 3. Access a protected endpoint without logging in
curl -sS -o /dev/null -w '%{http_code}\n' \
     https://<your-domain>/api/cameras                 # expect 401

Finally, walk through the whole flow in a browser: log in → see the feed in live view → no Mixed Content errors in the browser console. If you're kicked back to the login page right after logging in, or the live view reports Mixed Content, it's 99% that X-Forwarded-Proto wasn't passed correctly. For more troubleshooting, see Troubleshooting.