--- title: Expose Service on edge2 (Contabo Cloud VPS) type: runbook tags: - proxmox aliases: [] related: - [[lxc-service-migration]] - [[expose-service-home]] - [[expose-service-contabo]] - [[edge2-access-reference]] - [[caddy]] updated: 2026-08-24 --- # Expose Service on edge2 (Contabo Cloud VPS) ## Context edge2 is a Proxmox VE 8 node (184.174.35.153 / 100.64.0.26) running LXC containers on an internal bridge (`vmbr0`, subnet `10.10.10.0/24`, gateway `10.10.10.1`). [[services]] run inside unprivileged LXC containers. [[caddy]] on the edge2 host terminates TLS and reverse-proxies to the container's internal IP. ## Prerequisites - SSH access to edge2: `ssh edge2` (admin@100.64.0.26, key auth, passwordless sudo) - Debian 13 CT template cached: `local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst` - [[dns]] provider access (Cloudflare, GoDaddy, etc.) ## Steps ### 1. Create the LXC container Pick the next available CTID (current: 100=pdm, 101=wordpress). All CTs use static IPs on the `10.10.10.0/24` subnet. ```bash ssh edge2 sudo pct create local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \ --hostname \ --rootfs local: \ --memory \ --swap 512 \ --cores \ --net0 name=eth0,bridge=vmbr0,ip=10.10.10./24,gw=10.10.10.1 \ --nameserver 1.1.1.1 \ --unprivileged 1 \ --features nesting=1 \ --onboot 1 \ --start 1 # Verify sudo pct list sudo pct exec -- ping -c1 1.1.1.1 ``` ### 2. Install the service inside the container ```bash sudo pct exec -- bash # ... install service, bind to port on 0.0.0.0 or 10.10.10. # ... the host will reach the CT via the internal bridge ``` For file transfer into the CT: ```bash # From edge2 host sudo pct push /path/on/host /path/in/ct ``` ### 3. Add Caddy site block on edge2 host [[caddy]] runs on the edge2 host and terminates TLS. **For Cloudflare-proxied domains** (orange cloud / Full SSL mode): ```bash # tls internal generates a self-signed cert — Cloudflare "Full" mode # encrypts transit without needing a publicly-trusted cert sudo tee -a /etc/caddy/Caddyfile << 'EOF' { tls internal reverse_proxy 10.10.10.: } EOF sudo caddy validate --config /etc/caddy/Caddyfile sudo systemctl reload caddy ``` **For non-Cloudflare domains** (needs real cert): ```bash # Caddy will auto-provision a Let's Encrypt cert sudo tee -a /etc/caddy/Caddyfile << 'EOF' { reverse_proxy 10.10.10.: } EOF sudo caddy validate --config /etc/caddy/Caddyfile sudo systemctl reload caddy ``` ### 4. Configure DNS **GoDaddy domains:** ```bash # On cortex — godaddy-dns.py lives at ~/bin/godaddy-dns.py there and reads # credentials from .ref/credentials itself godaddy-dns.py add-a 184.174.35.153 ``` See [[dns]] for the record-set trap and full subcommand list. **Cloudflare domains:** Manual via Cloudflare dashboard (no API token): 1. A record → 184.174.35.153 (proxied / orange cloud) 2. SSL/TLS mode → Full (not Strict — since `tls internal` uses self-signed) 3. Optional: www CNAME → domain (proxied) ### 5. Verify ```bash # From edge2 (proper SNI required for tls internal) curl -sk --resolve :443:127.0.0.1 -o /dev/null -w "%{http_code}\n" https:/// # From cortex (direct to origin) curl -sk --resolve :443:184.174.35.153 -o /dev/null -w "%{http_code}\n" https:/// # Via DNS (after propagation) curl -I https:/// ``` ### 6. Update docs - Add CT to `environment.md` LXC Containers table - Add credentials to `.ref/credentials` - Add Tailscale IP if Tailscale is installed in the CT ## Checklist ``` □ CT created, started, networking verified (ping 1.1.1.1) □ Service installed and running inside CT □ Caddy site block added and reloaded on edge2 host □ DNS record pointing to 184.174.35.153 □ SSL mode correct (Full for Cloudflare, auto for others) □ HTTPS verified from edge2 + externally □ Docs updated (environment.md, credentials) ``` ## Network Reference | Resource | Value | |----------|-------| | edge2 public IP | 184.174.35.153 | | edge2 Tailscale | 100.64.0.26 | | Internal bridge | vmbr0, 10.10.10.0/24 | | Gateway | 10.10.10.1 (edge2 host) | | [[dns]] in CTs | 1.1.1.1 | | CT IP range | 10.10.10.10+ (10=pdm, 11=wordpress) | ## CT Creation via PVE API (alternative) When SSH is unavailable, CTs can be created via the PVE REST API: ```python import urllib.request, urllib.parse, json, ssl ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE base = "https://100.64.0.26:8006/api2/json" # Authenticate data = urllib.parse.urlencode({"username": "root@pam", "password": ""}).encode() req = urllib.request.Request(f"{base}/access/ticket", data=data) resp = urllib.request.urlopen(req, context=ctx) auth = json.loads(resp.read())["data"] ticket, csrf = auth["ticket"], auth["CSRFPreventionToken"] # Create CT params = urllib.parse.urlencode({ "vmid": , "hostname": "", "ostemplate": "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst", "cores": 2, "memory": 2048, "swap": 512, "rootfs": "local:16", "net0": "name=eth0,bridge=vmbr0,ip=10.10.10./24,gw=10.10.10.1", "nameserver": "1.1.1.1", "unprivileged": 1, "features": "nesting=1", "onboot": 1, "start": 1, }).encode() req = urllib.request.Request(f"{base}/nodes/edge2/lxc", data=params, method="POST") req.add_header("Cookie", f"PVEAuthCookie={ticket}") req.add_header("CSRFPreventionToken", csrf) resp = urllib.request.urlopen(req, context=ctx) print(json.loads(resp.read())["data"]) # UPID of creation task ``` Note: The PVE API uses `root@pam` auth. This is separate from the system SSH user (`admin`). See credentials file for the PVE password.