152 lines
4.5 KiB
Markdown
152 lines
4.5 KiB
Markdown
# VPS Reverse Proxy Migration
|
|
|
|
Full pattern for migrating from port-forwarded home server to VPS reverse proxy with Tailscale tunnel.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Internet → VPS (nginx SSL, ports 80/443)
|
|
↳ Tailscale tunnel (41641 UDP)
|
|
↳ Home server (zero ports open to internet)
|
|
```
|
|
|
|
## VPS Setup
|
|
|
|
### 1. Purchase VPS
|
|
|
|
OVH VPS Starter ($5/mo, 4GB RAM, 40GB NVMe, Ubuntu 26.04 LTS) or equivalent.
|
|
|
|
### 2. Install essentials
|
|
|
|
```bash
|
|
sudo apt update && sudo apt install -y nginx certbot python3-certbot-nginx curl ufw
|
|
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw allow 22/tcp
|
|
sudo ufw --force enable
|
|
```
|
|
|
|
### 3. Install Tailscale
|
|
|
|
```bash
|
|
curl -fsSL https://tailscale.com/install.sh | sudo sh
|
|
sudo tailscale up --accept-routes --accept-dns=false
|
|
# Authenticate at the URL shown
|
|
```
|
|
|
|
Verify: `tailscale status` should show home server (`rayserver`) online.
|
|
|
|
### 4. Domain DNS (Porkbun)
|
|
|
|
Two A records:
|
|
- `*` → VPS IP
|
|
- `@` (root) → VPS IP
|
|
|
|
Via Porkbun API:
|
|
```bash
|
|
curl -X POST "https://api.porkbun.com/api/json/v3/dns/create/DOMAIN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"apikey":"pk1_...","secretapikey":"sk1_...","name":"*","type":"A","content":"VPS_IP","ttl":300}'
|
|
```
|
|
|
|
Delete any default parking records (CNAME/ALIAS → uixie.porkbun.com) first.
|
|
|
|
### 5. Nginx reverse proxy template
|
|
|
|
Each service gets a subdomain config:
|
|
```nginx
|
|
server {
|
|
listen 80;
|
|
server_name <name>.graj-media.com;
|
|
client_max_body_size 500M;
|
|
location / {
|
|
proxy_pass http://100.93.253.36:<port>;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
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-Proto $scheme;
|
|
proxy_read_timeout 86400;
|
|
proxy_buffering off;
|
|
}
|
|
}
|
|
```
|
|
|
|
For services behind home nginx SSL (Mealie, ShopProQuote), use HTTPS proxy:
|
|
```nginx
|
|
proxy_pass https://100.93.253.36:3449;
|
|
proxy_ssl_verify off;
|
|
proxy_ssl_server_name on;
|
|
proxy_ssl_name graj-media.com;
|
|
```
|
|
|
|
### 6. SSL via Let's Encrypt
|
|
|
|
Single cert for all subdomains:
|
|
```bash
|
|
sudo certbot --nginx -d graj-media.com \
|
|
-d immich.graj-media.com -d paperless.graj-media.com \
|
|
-d ha.graj-media.com -d shopproquote.graj-media.com \
|
|
-d mealie.graj-media.com -d audiobookshelf.graj-media.com
|
|
```
|
|
|
|
Auto-renews via systemd timer.
|
|
|
|
### 7. LLM proxy (ShopProQuote)
|
|
|
|
Add `/llm/` location block to ShopProQuote config:
|
|
```nginx
|
|
location /llm/ {
|
|
proxy_pass http://100.93.253.36:11434/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_read_timeout 120;
|
|
}
|
|
```
|
|
|
|
### 8. Home Assistant trusted proxies
|
|
|
|
HA behind VPS reverse proxy returns 400 without trusted proxy config:
|
|
```yaml
|
|
http:
|
|
use_x_forwarded_for: true
|
|
trusted_proxies:
|
|
- 127.0.0.1
|
|
- 100.86.68.23 # VPS Tailscale IP
|
|
```
|
|
Restart: `docker restart homeassistant`
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
# Check all services from VPS
|
|
for url in \
|
|
https://graj-media.com \
|
|
https://immich.graj-media.com \
|
|
https://paperless.graj-media.com \
|
|
https://ha.graj-media.com \
|
|
https://shopproquote.graj-media.com \
|
|
https://mealie.graj-media.com \
|
|
https://audiobookshelf.graj-media.com; do
|
|
curl -skL -o /dev/null -w "%{http_code} $url\n" $url
|
|
done
|
|
```
|
|
|
|
## Close home router ports
|
|
|
|
After verifying all services work via VPS, delete ALL port forwarding rules on the ASUS RT-AX82U router. Home server should have zero ports exposed.
|
|
|
|
```bash
|
|
# Verify from outside
|
|
for port in 80 443 2283 3443 3444 3445 3446 3447 3448 3449 3450 8010 8123 13378; do
|
|
timeout 2 bash -c "echo >/dev/tcp/HOME_PUBLIC_IP/$port" 2>/dev/null && echo "PORT $port OPEN" || echo "PORT $port closed"
|
|
done
|
|
```
|
|
|
|
## Pitfalls
|
|
|
|
- **Bash variable expansion in heredocs.** When writing nginx configs via `ssh host "sudo tee << 'EOF' ..."`, `$http_upgrade` and `$host` are expanded by the local shell despite the quoted EOF marker. Workaround: write to temp file locally, scp, or use sed to fix after writing.
|
|
- **Certbot namespace collision.** Old certbot configs for DuckDNS domains conflict with new ones. Delete all old certs and configs before re-issuing for the new domain.
|
|
- **Mealie direct port is localhost-only.** Mealie Docker binds `127.0.0.1:9925`, unreachable via Tailscale. Proxy through the home nginx SSL endpoint instead (`https://100.93.253.36:3449`).
|
|
- **DuckDNS 5-domain limit.** Can't use subdomain-per-service with DuckDNS. A proper domain ($11/yr) with wildcard DNS is required.
|