# How to Set Up Tailscale + Apache Guacamole on Oracle Cloud for Browser-Based Remote Desktop
Date: 2026-07-14
## Why This Stack
You want to remote desktop into your home machines from a browser. No VPN client, no port forwarding, no TeamViewer "commercial use" lockouts. Here is the stack that works:
- **Oracle Cloud free tier ARM instance** — public IP, always free, 4 cores / 24 GB RAM
- **Tailscale** — mesh VPN. All your machines connect to Tailscale. No open ports.
- **Apache Guacamole** — clientless RDP/VNC/SSH gateway. Runs as a Docker container.
- **Nginx Proxy Manager** — TLS termination, Let's Encrypt SSL, reverse proxy rules.
- **Authentik** — SSO forward-auth (optional but recommended: no one reaches Guacamole without logging in first).
The flow: browser → `rdp.yourdomain.com` → NPM (TLS) → Authentik (auth) → Guacamole → Tailscale → your internal machine.
## Step 1: Oracle Cloud Free Tier Instance
Sign up at cloud.oracle.com. Use the "Always Free" ARM Ampere A1 instance:
```
Shape: VM.Standard.A1.Flex
OCPU: 4
Memory: 24 GB
Boot volume: 100 GB
OS: Ubuntu 22.04 LTS
```
After provisioning, assign a public IP (Ephemeral or Reserved). Note it — this is your entry point.
### Initial firewall
Oracle uses Security Lists, not iptables. In the OCI console, add ingress rules:
| Source | Protocol | Port | Purpose |
|---|---|---|---|
| 0.0.0.0/0 | TCP | 80 | NPM HTTP |
| 0.0.0.0/0 | TCP | 443 | NPM HTTPS |
| 0.0.0.0/0 | TCP | 81 | NPM admin (restrict to your IP later) |
Do NOT open 22 to the world. SSH access goes through Tailscale only (Step 2).
## Step 2: Install Tailscale on Every Machine
On the Oracle instance:
```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --accept-routes --accept-dns
```
On every internal machine you want to remote into (Windows desktop, Linux server, Raspberry Pi):
```bash
# Same install command
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
```
Now lock down SSH on Oracle to Tailscale only. Create `/etc/ssh/sshd_config.d/99-tailscale.conf`:
```
Match Address 100.0.0.0/8
PasswordAuthentication yes
Match all
PasswordAuthentication no
```
This means: SSH from Tailscale IPs (100.x.x.x) is allowed. Everything else is rejected. Your public IP still shows port 22 closed — Oracle's security list already blocks it.
### Verify
From your laptop (connected to Tailscale):
```bash
ssh ubuntu@100.87.69.61 # Oracle's Tailscale IP
```
From outside Tailscale:
```bash
ssh ubuntu@192.9.163.106 # Oracle's public IP — blocked
```
## Step 3: Nginx Proxy Manager
On Oracle, create a docker-compose for NPM:
```yaml
# stacks/nginx-proxy-manager.yml
version: '3'
services:
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:2.10.3
container_name: npm
restart: unless-stopped
ports:
- "80:80"
- "81:81"
- "443:443"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
```
```bash
docker compose -f stacks/nginx-proxy-manager.yml up -d
```
Access NPM admin at `http://:81`. Default login: `admin@example.com` / `changeme`. Change immediately.
### DNS setup
Create an A record pointing `rdp.yourdomain.com` to your Oracle public IP. (For the rest of this guide, substitute your actual domain.)
### SSL certificate
In NPM → SSL Certificates → Add Let's Encrypt → enter `rdp.yourdomain.com`. NPM handles renewal automatically.
## Step 4: Apache Guacamole
Guacamole has two components: `guacd` (the backend daemon) and `guacamole` (the web frontend). Run both on Oracle:
```yaml
# stacks/guacamole.yml
version: '3'
services:
guacd:
image: guacamole/guacd:1.5.5
container_name: guacd
restart: unless-stopped
volumes:
- ./guacd/drive:/drive
- ./guacd/recordings:/recordings
guacamole:
image: guacamole/guacamole:1.5.5
container_name: guacamole
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
GUACD_HOSTNAME: guacd
MYSQL_HOSTNAME: guacamole-db
MYSQL_DATABASE: guacamole_db
MYSQL_USER: guacamole_user
MYSQL_PASSWORD: CHANGE_ME
depends_on:
- guacd
- guacamole-db
guacamole-db:
image: mysql:8.0
container_name: guacamole-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: CHANGE_ME_ROOT
MYSQL_DATABASE: guacamole_db
MYSQL_USER: guacamole_user
MYSQL_PASSWORD: CHANGE_ME
volumes:
- ./guacamole-db:/var/lib/mysql
```
Note: Guacamole only listens on `127.0.0.1:8080` — not exposed to the internet. NPM proxies to it.
### Initialize the database
```bash
docker run --rm guacamole/guacamole:1.5.5 /opt/guacamole/bin/initdb.sh --mysql > initdb.sql
# Import into the guacamole-db container
```
### Add your connections
In the Guacamole web UI (once proxied), add connections pointing to Tailscale IPs:
| Connection name | Protocol | Host | Port |
|---|---|---|---|
| Dev Desktop | RDP | 100.x.x.x (your desktop's Tailscale IP) | 3389 |
| Ubuntu Server | SSH | 100.89.58.49 | 22 |
| Home Laptop | VNC | 100.x.x.x | 5900 |
Because everything is on the Tailscale network, Guacamole on Oracle can reach any machine with a Tailscale IP. No port forwarding, no dynamic DNS.
## Step 5: Authentik SSO (Optional but Recommended)
Without Authentik, anyone who finds your Guacamole URL sees the login page. With Authentik, they don't even reach Guacamole — they get redirected to an SSO login first.
```yaml
# Include in existing Authentik stack or create standalone
services:
authentik-server:
image: ghcr.io/goauthentik/server:2024.8
container_name: authentik-server
# ... full Authentik config
```
In NPM → Proxy Hosts → `rdp.yourdomain.com` → Advanced:
```
location / {
# Forward-auth to Authentik
auth_request /outpost.goauthentik.io/auth/nginx;
error_page 401 = @goauthentik_proxy_signin;
# If auth passes, proxy to Guacamole
proxy_pass http://127.0.0.1:8080/guacamole/;
}
location /outpost.goauthentik.io/ {
proxy_pass http://authentik-server:9000/outpost.goauthentik.io/;
}
```
Result: user opens `rdp.yourdomain.com` → Authentik login screen → if valid, page loads Guacamole. No Guacamole login page is ever exposed to the internet.
## Step 6: Access From Anywhere
On any device with a browser:
1. Open `https://rdp.yourdomain.com`
2. Sign in with Authentik (if configured) or Guacamole credentials
3. Select your connection from the Guacamole dashboard
4. Desktop appears in the browser — fully interactive, no client install
Works on phones, tablets, locked-down work laptops, library computers. All you need is a browser.
## Total cost
| Component | Cost |
|---|---|
| Oracle Cloud ARM instance | $0 (Always Free tier) |
| Domain (yourdomain.com) | ~$15/year |
| Tailscale (personal, up to 100 devices) | $0 |
| Guacamole, NPM, Authentik | $0 (open source) |
| **Total** | **~$15/year** |
## Why not alternatives?
| Alternative | Problem |
|---|---|
| Tailscale Funnel | Exposes internal services directly to internet. No auth layer. Limited to 3 ports. |
| Chrome Remote Desktop | Chrome must be running. No Linux server support. Google account dependency. |
| TeamViewer | Expensive. Flags personal use as "commercial" randomly. Proprietary. |
| Plain RDP port forward | Dangerous. RDP has a massive attack surface. Brute-force magnet. |
## Troubleshooting
**Guacamole connection fails:** verify Tailscale is running on both Oracle and the target machine (`tailscale status`). Ping the Tailscale IP from Oracle. If it fails, check `tailscale up --accept-routes` was used.
**NPM returns 502:** Guacamole container not reachable at `127.0.0.1:8080`. Check `docker ps` — is `guacamole` running? Check logs: `docker logs guacamole`.
**Authentik returns 404:** outpost not configured. Verify the Authentik outpost is set to proxy mode and the provider is linked to the application.
**SSH blocked from public IP:** working as designed. Use the Tailscale IP.
Why This Stack?
For the architectural reasoning behind each component choice — why Oracle over AWS, why Tailscale over plain WireGuard, why Guacamole over RDP — see the companion article: Reliable Remote Desktop at Home with Tailscale and Guacamole.
Leave a Reply