Files changed: engine/.embcache.json engine/changelog.md engine/lint-report.md vault/.obsidian/workspace.json vault/docs/services/services.md vault/docs/software/caddy.md vault/docs/software/dns.md vault/runbooks/expose-service-edge2.md vault/runbooks/expose-service-home.md
5.7 KiB
| title | type | tags | aliases | related | updated | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Expose Service on edge2 (Contabo Cloud VPS) | runbook |
|
|
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.
ssh edge2
sudo pct create <CTID> local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
--hostname <hostname> \
--rootfs local:<DISK_GB> \
--memory <RAM_MB> \
--swap 512 \
--cores <CORES> \
--net0 name=eth0,bridge=vmbr0,ip=10.10.10.<X>/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 <CTID> -- ping -c1 1.1.1.1
2. Install the service inside the container
sudo pct exec <CTID> -- bash
# ... install service, bind to port on 0.0.0.0 or 10.10.10.<X>
# ... the host will reach the CT via the internal bridge
For file transfer into the CT:
# From edge2 host
sudo pct push <CTID> /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):
# 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'
<domain> {
tls internal
reverse_proxy 10.10.10.<X>:<PORT>
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
For non-Cloudflare domains (needs real cert):
# Caddy will auto-provision a Let's Encrypt cert
sudo tee -a /etc/caddy/Caddyfile << 'EOF'
<domain> {
reverse_proxy 10.10.10.<X>:<PORT>
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
4. Configure DNS
GoDaddy domains:
# On cortex — godaddy-dns.py lives at ~/bin/godaddy-dns.py there and reads
# credentials from .ref/credentials itself
godaddy-dns.py add-a <zone> <subdomain> 184.174.35.153
See dns for the record-set trap and full subcommand list.
Cloudflare domains: Manual via Cloudflare dashboard (no API token):
- A record → 184.174.35.153 (proxied / orange cloud)
- SSL/TLS mode → Full (not Strict — since
tls internaluses self-signed) - Optional: www CNAME → domain (proxied)
5. Verify
# From edge2 (proper SNI required for tls internal)
curl -sk --resolve <domain>:443:127.0.0.1 -o /dev/null -w "%{http_code}\n" https://<domain>/
# From cortex (direct to origin)
curl -sk --resolve <domain>:443:184.174.35.153 -o /dev/null -w "%{http_code}\n" https://<domain>/
# Via DNS (after propagation)
curl -I https://<domain>/
6. Update docs
- Add CT to
environment.mdLXC 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:
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": "<EDGE2_ROOT_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": <CTID>, "hostname": "<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.<X>/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.