name: homelab-deploy description: Deploy a service or static site publicly via CasaOS + VPS reverse proxy. Covers DNS, SSL, nginx config, Docker containers, and static files.
Homelab Deploy Workflow
Deploys services from the homelab to the public internet via:
Client → VPS nginx (104.168.64.181) → [VPS local | CasaOS:port via WireGuard]
Credentials & Access
- VPS SSH:
root@104.168.64.181 - CasaOS SSH:
nerfherder@farmstand(Tailscale only — LAN IP 192.168.50.19 unreachable from this machine) - Porkbun API: stored in Recall — query
porkbun API key - Domain:
streamy.tube(all public subdomains live here)
Decision: VPS-local vs CasaOS tunnel
| Type | Host on VPS | Host on CasaOS |
|---|---|---|
| Static files (HTML, SPA build) | ✅ Preferred | ❌ Overkill |
| Simple pass-through (no state) | ✅ OK | ✅ OK if port is open |
| Stateful service (DB, media, AI) | ❌ | ✅ Required |
WireGuard tunnel constraint: Only ports 80 and 9010 on CasaOS are currently reachable from the VPS via WireGuard. Any new CasaOS service on a custom port requires opening that port on OPNsense (Interfaces → WireGuard → Firewall rules).
Path A: Static File (serve from VPS)
1. Copy file to VPS
scp /path/to/file.html root@104.168.64.181:/var/www/{name}/index.html
# Or for a build directory:
scp -r dist/ root@104.168.64.181:/var/www/{name}/
2. Add DNS A record via Porkbun API
curl -s -X POST "https://api.porkbun.com/api/json/v3/dns/create/streamy.tube" \
-H "Content-Type: application/json" \
-d '{
"apikey": "PORKBUN_API_KEY",
"secretapikey": "PORKBUN_SECRET_KEY",
"type": "A",
"name": "SUBDOMAIN",
"content": "104.168.64.181",
"ttl": "300"
}'
3. HTTP-only nginx + certbot
# Write HTTP config first so certbot can do ACME challenge
ssh root@104.168.64.181 "cat > /etc/nginx/sites-enabled/{subdomain}.streamy.tube << 'EOF'
server {
listen 80; listen [::]:80;
server_name {subdomain}.streamy.tube;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://\$host\$request_uri; }
}
EOF
nginx -t && systemctl reload nginx"
# Issue cert
ssh root@104.168.64.181 "certbot certonly --nginx -d {subdomain}.streamy.tube --non-interactive --agree-tos"
4. Full HTTPS config (static)
server {
listen 443 ssl; listen [::]:443 ssl;
server_name {subdomain}.streamy.tube;
ssl_certificate /etc/letsencrypt/live/{subdomain}.streamy.tube/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{subdomain}.streamy.tube/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/{name};
index index.html;
location / { try_files $uri $uri/ =404; }
}
server {
listen 80; listen [::]:80;
server_name {subdomain}.streamy.tube;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }
}
5. Test
curl -sI https://{subdomain}.streamy.tube | head -3
Path B: CasaOS Docker Service (tunnel through WireGuard)
1. Deploy container on CasaOS
ssh nerfherder@farmstand "mkdir -p /DATA/AppData/{name}"
# Write docker-compose.yml, then:
ssh nerfherder@farmstand "cd /DATA/AppData/{name} && docker compose up -d"
2. Open port on OPNsense (if not port 80)
Firewall → Rules → WireGuard interface → Add rule:
- Protocol: TCP, Destination: CasaOS (192.168.50.19), Port: {service_port}
3. Add DNS + VPS nginx (proxy config)
Same DNS step as Path A, then use proxy nginx template:
upstream {name} {
server 192.168.50.19:{port};
keepalive 16;
}
server {
listen 443 ssl; listen [::]:443 ssl;
server_name {subdomain}.streamy.tube;
ssl_certificate /etc/letsencrypt/live/{subdomain}.streamy.tube/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{subdomain}.streamy.tube/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://{name};
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-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
}
}
Existing VPS Nginx Sites
All configs at /etc/nginx/sites-enabled/ on VPS. Pattern: one file per subdomain.
Currently deployed: authentik, casa, code-server, codevv, jellyfin, jellyseerr, livekit, mural, n8n, recall, revive, riven, riven-api, sim
Porkbun API Reference
- Create:
POST /api/json/v3/dns/create/{domain} - Delete:
POST /api/json/v3/dns/delete/{domain}/{record_id} - List:
POST /api/json/v3/dns/retrieve/{domain} - Response includes
id— save it if you need to delete later