Remote Access

By default SkyView is only available on the LAN — once you leave home, the mobile app can't reach your home server. This chapter gives three ways to expose SkyView to the internet securely: mesh VPN (recommended), reverse proxy, and port forwarding, and explains which ports each one needs.

Read this first

SkyView listens on 0.0.0.0 by default with no built-in firewall rules. Exposing ports bare to the internet = every scanner on Earth can reach your cameras. Any remote-access scheme must be paired with HTTPS + a strong password, and avoid bare public exposure whenever possible.

Choosing among the three methods

MethodSecurityDifficultyPublic exposureBest for
Mesh VPNHighestLowNoThe vast majority of home users (recommended)
Reverse proxy + domainMediumMediumYes (443 only)Wanting a domain, or sending a link to family who can't easily install the app
Port forwardingLowLowYes (bare exposure)Not recommended; temporary only / when you fully understand the risk

If unsure, use mesh VPN: open no ports on the router, buy no domain, configure no certificates — highest security and almost zero ops. The three sections below expand on each.

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.

Why mesh VPN is the top recommendation

Not opening ports on the router = zero attack surface to the internet; scanners simply can't find you. It also doesn't depend on a public IP — many broadband connections are CGNAT with no public IP at all, and mesh punches through anyway. There's an easily-overlooked bonus: on a mesh, the phone is treated as on the LAN, so live view defaults to the WebRTC low-latency channel (sub-second latency, instant start), whereas reverse proxy / port forwarding can only use HLS (~3 seconds latency) — so mesh isn't just the most secure, it's also the most responsive for live view. The only cost is installing the mesh app once on each device you want to use.

Method 2: 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.

Only one port to forward

The container converges the Web Admin, API, and live streams (HLS + WebRTC signaling) onto the single port :23406. The outer proxy only forwards this one port — don't separately expose mediamtx's 24214 / 24215. WebRTC live video uses UDP :23515, which doesn't go through the proxy, but on the internet this UDP path usually can't be established — live view over the internet auto-falls back to HLS, so not forwarding 23515 is fine; see the note at the end of "Method 2" below.

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

Live view over the internet uses HLS — don't fuss over WebRTC

With the reverse-proxy scheme, the router only needs to forward public 443/tcp to 443 on the proxy machine. `23515/udp` doesn't need forwarding: WebRTC low-latency live relies on mediamtx advertising "reachable" ICE candidate addresses, but SkyView only advertises LAN addresses by default (the webrtcAdditionalHosts and STUN needed for public NAT traversal aren't configured), so under a home NAT the external client can't obtain a usable WebRTC channel, and forwarding 23515 is pointless. Live view over the internet auto-degrades to HLS (~3 seconds latency, fully functional); for WebRTC's instant-start low latency, use Method 1, mesh VPN — on a mesh the phone is treated as on the LAN and WebRTC works normally.

Understand the risk before using

Port forwarding maps the server's port straight to the internet, where anyone can access it. Forwarding 23406 bare is plaintext HTTP — passwords and the feed are unencrypted end to end, and browsers and Android 9+ refuse plaintext HTTP requests for camera permissions. Only consider it when you fully understand the risk and it's temporary; for long-term use, switch to mesh VPN or a reverse proxy.

If you insist on port forwarding, we strongly recommend also running a layer of reverse proxy on the SkyView host for HTTPS (see Method 2), forwarding public 443 to the proxy rather than exposing 23406 bare. The ports to forward:

Public port→ internalProtocolNotes
443Proxy host 443TCPDo HTTPS via the proxy, then forward to 23406 (recommended approach)
23406SkyView host 23406TCPThe bare-forward port without a proxy, plaintext HTTP, not recommended

Never forward 24214 / 24215

Live streams already go through the proxy on 23406 with a live-grant token. Exposing mediamtx's 24214 (HLS) / 24215 (WebRTC signaling) bare to the internet bypasses token auth and is a security hole. The container's mediamtx.yml already restricts these two ports to 127.0.0.1 only.

Port cheat sheet

PortProtocolPurposeOpen to the internet?
23406TCPWeb Admin + API + live-stream token proxy (the only HTTP entry)Required (with a proxy you forward 443)
23515UDPWebRTC live-view media stream (works on LAN / mesh)No need (internet live auto-uses HLS)
23880TCPmediamtx RTSP, for cameras / external players to connect directlyNo need
24214TCPmediamtx HLS (127.0.0.1 only)Forbidden
24215TCPmediamtx WebRTC signaling (127.0.0.1 only)Forbidden

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.

Remote Access - SkyView Docs