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
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
| Method | Security | Difficulty | Public exposure | Best for |
|---|---|---|---|---|
| Mesh VPN | Highest | Low | No | The vast majority of home users (recommended) |
| Reverse proxy + domain | Medium | Medium | Yes (443 only) | Wanting a domain, or sending a link to family who can't easily install the app |
| Port forwarding | Low | Low | Yes (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.
Method 1: mesh VPN (recommended)
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
Install the mesh client on the server
Install Tailscale on the machine running SkyView (or a soft router on the same LAN), then
tailscale upto log in. WireGuard / ZeroTier are similar.bashcurl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up - 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
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 uses10.x.x.x— the port is still:23406, e.g.http://100.x.x.x:23406. - 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
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
: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.
# 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;
}
}X-Forwarded-Proto / X-Forwarded-Host must be passed + included in every location
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):
certbot --nginx -d <your-domain> -m <your-email> --agree-tos --no-eff-email --redirectCaddy config (simpler)
Caddy auto-issues / renews Let's Encrypt certificates without certbot, and the config is much shorter. /etc/caddy/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
}
}Live view over the internet uses HLS — don't fuss over WebRTC
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.Method 3: port forwarding (not recommended)
Understand the risk before using
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 | → internal | Protocol | Notes |
|---|---|---|---|
| 443 | Proxy host 443 | TCP | Do HTTPS via the proxy, then forward to 23406 (recommended approach) |
| 23406 | SkyView host 23406 | TCP | The bare-forward port without a proxy, plaintext HTTP, not recommended |
Never forward 24214 / 24215
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
| Port | Protocol | Purpose | Open to the internet? |
|---|---|---|---|
| 23406 | TCP | Web Admin + API + live-stream token proxy (the only HTTP entry) | Required (with a proxy you forward 443) |
| 23515 | UDP | WebRTC live-view media stream (works on LAN / mesh) | No need (internet live auto-uses HLS) |
| 23880 | TCP | mediamtx RTSP, for cameras / external players to connect directly | No need |
| 24214 | TCP | mediamtx HLS (127.0.0.1 only) | Forbidden |
| 24215 | TCP | mediamtx WebRTC signaling (127.0.0.1 only) | Forbidden |
How to verify after configuring
# 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 401Finally, 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.