Date: 2026-07-08
The Problem
Remote desktop to home machines is a mess of half-working solutions. RDP needs port forwarding on the router. VNC needs a VPN or SSH tunnel. TeamViewer works until it flags you as “commercial use” and locks you out. Chrome Remote Desktop needs Chrome running. And none of these work cleanly across Windows, Linux, and containers simultaneously.
I wanted one thing: open a browser, type a URL, and be looking at my desktop — any machine, anywhere, no client software.
The Architecture
Browser (any device)
│
│ https://desktop.yourdomain.com
▼
┌─────────────────────────────────┐
│ CLOUD VPS │
│ Public IP │
│ Tailscale: 100.x.x.x │
│ │
│ ┌───────────────────────────┐ │
│ │ Nginx Proxy Manager │ │
│ │ TLS termination │ │
│ │ Let's Encrypt SSL │ │
│ └───────┬───────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ Apache Guacamole │ │
│ │ guacd + web UI │ │
│ │ HTML5 RDP/VNC/SSH gateway │ │
│ └───────┬───────────────────┘ │
└──────────┼──────────────────────┘
│
Tailscale mesh network
(WireGuard under the hood)
│
┌─────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌────────────────┐
│Windows PC │ │ Ubuntu │ │ Dev Webtop │
│Windows │ │ Server │ │ Containers │
│10 Pro │ │(Docker) │ │ (XRDP) │
│192.168 │ │100.89.x │ │ 100.x.x.x │
└─────────┘ └──────────┘ └────────────────┘
No port forwarding on the home router. No dynamic DNS on the home connection. No reverse SSH tunnels. Every machine simply runs Tailscale, and they all reach each other directly over WireGuard.
Why This Stack
Cloud VPS with Public IP
You need exactly one cloud VPS with a public IP. Any provider works — a $5/month Lightsail droplet, a Hetzner VM, or whatever you already run. The only requirement: it runs Tailscale and has a public IP.
Tailscale
Every device gets a static `100.x.x.x` IP on the Tailscale mesh. No config, no subnet routing, no firewall rules. Install the client, run `tailscale up`, and every device can reach every other device.
For the Guacamole server on the VPS, this means it can open RDP connections to `100.x.x.x:3389` (Ubuntu Server) just as easily as `192.168.1.x:3389` (a Windows machine on the local network). Tailscale handles the NAT traversal and encryption.
Apache Guacamole
Guacamole is a clientless remote desktop gateway. It runs as a web application — users connect via browser, and Guacamole proxies the connection to the target machine using standard protocols (RDP, VNC, SSH). No client software needed on the connecting device — not even a browser extension.
The stack is two containers:
# docker-compose.yml (cloud instance)
services:
guacd:
image: guacamole/guacd:latest
restart: unless-stopped
guacamole:
image: guacamole/guacamole:latest
restart: unless-stopped
environment:
GUACD_HOSTNAME: guacd
MYSQL_HOSTNAME: mysql
MYSQL_DATABASE: guacamole_db
MYSQL_USER: guacamole_user
MYSQL_PASSWORD: ${GUAC_MYSQL_PASSWORD}
depends_on:
- guacd
- mysql
networks:
- proxy_shared
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: guacamole_db
MYSQL_USER: guacamole_user
MYSQL_PASSWORD: ${GUAC_MYSQL_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
Guacd is the native proxy daemon — it handles the actual RDP/VNC/SSH connections. The Guacamole web app is the Java servlet that renders the HTML5 canvas in the browser.
Nginx Proxy Manager
NPM sits in front of Guacamole, handling TLS termination with Let’s Encrypt and routing `desktop.yourdomain.com` to the Guacamole container. It also proxies WebSocket connections, which Guacamole needs for the real-time screen updates.
Client -> NPM:443 -> guacamole:8080 (HTTP + WebSocket)
Connecting to an Internal Machine
In Guacamole’s admin interface, you define connections. Each connection specifies:
– **Protocol:** RDP
– **Host:** `100.x.x.x` (Tailscale IP of target machine)
– **Port:** `3389`
– **Username / Password:** your Windows or Linux credentials
That’s it. From a browser on your phone at a cafe, you navigate to `https://desktop.yourdomain.com`, log in, and click your desktop connection. Guacamole on the VPS opens an RDP session to the target via its Tailscale IP. The traffic flows: `the VPS → Tailscale mesh → target machine`. No ports exposed to the internet.
SSH Hardening
With a public-facing cloud instance, SSH is the attack surface. The fix: Tailscale-only SSH.
In `/etc/ssh/sshd_config.d/99-tailscale-only.conf`:
Match Address 127.0.0.1,::1,100.0.0.0/8
PubkeyAuthentication yes
This means:
– Connections from `localhost` → allowed (for Docker health checks, automation)
– Connections from `100.x.x.x` (Tailscale IP range) → allowed (your machines)
– Connections from any other IP → rejected before authentication
A bot scanning for SSH on the public IP gets `Connection refused`. You connect via Tailscale IP and it works.
The Full Request Flow
1. Browser → https://desktop.yourdomain.com
DNS resolves to the VPS public IP
2. the VPS public IP:443 → Nginx Proxy Manager
TLS terminated, Let's Encrypt certificate
3. NPM → guacamole:8080
Routes by host header, enables WebSocket
4. Guacamole web UI → user logs in
MySQL-backed auth, connection list loaded
5. User clicks "Windows Desktop" connection
6. Guacd → 192.168.1.x:3389 (RDP)
Traffic flows over Tailscale mesh
Tailscale handles NAT traversal + encryption
7. Guacd receives RDP framebuffer
Encodes as Guacamole protocol over WebSocket
8. Browser receives Guacamole protocol
Renders as HTML5 canvas — real-time screen
Why Not Just Tailscale Funnel?
Tailscale Funnel exposes a single port from any machine to the internet — you could theoretically point it at a Guacamole instance. But:
– Funnel has bandwidth limits (not suitable for streaming desktop video)
– Funnel requires the Tailscale client on every device that connects
– You lose the ability to add Authentik SSO, rate limiting, or custom auth in front of Guacamole
– Funnel means every internal machine exposes something — NPM centralizes that to the VPS
The VPS + NPM + Guacamole approach gives you one public surface area to secure, one place to manage TLS certs, and one auth gateway.
Adding Authentik for SSO
Once NPM is in front of Guacamole, adding Authentik forward-auth is one checkbox:
NPM Proxy Host → Advanced → Forward Auth:
http://authentik-server:9000/outpost.goauthentik.io/auth/nginx
Now your Guacamole login is protected by the same Authentik SSO that protects everything else on yourdomain.com. Users authenticate once, then access Guacamole, Nextcloud, Jellyfin, and the gridfinity generator with the same session.
What This Costs
| Component | Cost |
| Cloud VPS (any provider) | ~$5/month |
| Tailscale (personal plan) | Free (up to 3 users, 100 devices) |
| Guacamole (self-hosted) | Free (Apache 2.0) |
| Nginx Proxy Manager (self-hosted) | Free (MIT) |
| Domain (yourdomain.com) | ~$15/year |
| Let’s Encrypt SSL certs | Free |
Total: $15/year. For browser-based remote desktop to every machine you own, from any device, anywhere.
Is It Production-Ready?
Yes. I use this daily to access Windows desktop, manage Docker containers via browser-based terminals, and troubleshoot home server issues from my phone.
The weak point is the VPS — if it goes down, remote access goes down (but local access still works). Mitigation: a $5/month Lightsail instance as a failover, or the Tailscale MagicDNS fallback for direct connections.
The other limitation: RDP quality depends on the Tailscale connection latency. For text-based work and system administration, it’s perfect. For watching videos or gaming, it’s not — but that’s not what this is for.
Setting It Up Yourself
1. **Deploy a cloud VPS with Ubuntu 22.04 and a public IP**
2. **Install Tailscale** on the VPS and every internal machine:
“`bash
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
“`
3. **Deploy Nginx Proxy Manager** via Docker — point your domain’s DNS to the VPS public IP
4. **Deploy Guacamole** (guacd + web UI + MySQL) via Docker Compose
5. **Add Guacamole as a proxy host** in NPM — enable WebSocket support
6. **Configure RDP connections** in Guacamole admin — use Tailscale IPs as hosts
7. **Harden SSH** — `Match Address 100.0.0.0/8` to restrict to Tailscale only
8. **Optional: Add Authentik forward-auth** for SSO
The full stack runs alongside your existing services on the same VPS.
Leave a Reply