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
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
| Method | What you need | Router work | Live latency | Best for |
|---|---|---|---|---|
| Public direct access | A domain (a free subdomain works); the certificate is issued automatically | IPv4: one TCP forward rule. IPv6: just allow inbound | WebRTC, under 0.5 s | Connections with a public IP or IPv6 (recommended) |
| Mesh VPN | The same mesh app installed on every device you use | Nothing at all | WebRTC, sub-second | Carrier-grade NAT with no IPv6, or if you refuse to open any port |
| Reverse proxy + domain | An nginx / Caddy / Lucky instance you already run | Forward whichever port the proxy uses | HLS, 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.
Method 1: public direct access (recommended)
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
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.pemandprivkey.pemintodata/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
Step 2: pick a public port
On residential lines, think twice before using 443
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/tcpto23443on the SkyView host. That is the only rule — you do not need to forward23515or anything else. - IPv6: there is no NAT, so nothing to forward. Just allow inbound connections to
23443/tcpin 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
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
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.
The trade-off
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
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.
# 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
}
}Port cheat sheet
| Port | Protocol | Purpose | Open to the internet? |
|---|---|---|---|
23443 | TCP | Public direct access: web UI + API + live video (HTTPS, port configurable) | Yes — the only port this method needs |
23406 | TCP | LAN entry point (plain HTTP) | No — use 23443 above for remote access |
23515 | UDP + TCP | Live media stream (LAN / mesh / IPv6 direct) | No |
23880 | TCP | RTSP for external players on the LAN | No |
24214 | TCP | Internal streaming service | Never |
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
# 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.