echo6-docs/vault/runbooks/expose-service-edge2.md
echo6-autocommit ef8b1e0bd9 auto: docs sync 2026-07-13T12:00:23+00:00
Files changed: engine/.embcache.json engine/changelog.md engine/lint-report.md vault/.trash/2026-06-19.md vault/docs/hardware/environment.md vault/docs/hardware/ip-allocation.md vault/docs/matrix/archivist.md vault/docs/matrix/matrix_host.md vault/docs/matrix/mautrix_signal.md vault/docs/matrix/synapse.md vault/docs/matrix/synapse_retention_discovery.md vault/docs/navi/cc-rules.md vault/docs/navi/deployment.md vault/docs/navi/themes.md vault/docs/services/ots-setup.md vault/docs/services/services.md vault/docs/services/usenet.md vault/docs/software/authentik.md vault/docs/software/caddy.md vault/docs/software/central.md vault/docs/software/dns.md vault/docs/software/geo-tools.md vault/docs/software/navi.md vault/docs/software/recon.md vault/docs/software/searxng.md vault/glossary.md vault/notes/echo6-landing-page-data-export.md vault/notes/ia-download-queue.md vault/projects/advbbs-project.md vault/projects/argus.md vault/projects/deploy-livesync.md vault/projects/fleet-patch-audit.md vault/projects/fleet-platform-baseline.md vault/projects/matrix-synapse-deployment.md vault/projects/meshai-config-hot-apply.md vault/projects/meshai-region-routing-plan.md vault/projects/meshai.md vault/projects/meshcore-transport.md vault/projects/meshtastic-headscale-runbook.md vault/projects/mmud-project.md vault/projects/nominatim-v5-reimport.md vault/runbooks/add-peertube-channel.md vault/runbooks/authentik-access-groups.md vault/runbooks/authentik-create-invitation.md vault/runbooks/authentik-oidc-application.md vault/runbooks/authentik-upgrade.md vault/runbooks/central-deploy-cutover.md vault/runbooks/ct-runbook.md vault/runbooks/edge2-access-reference.md vault/runbooks/expose-service-contabo.md vault/runbooks/expose-service-edge2.md vault/runbooks/expose-service-home.md vault/runbooks/fleet-magicdns-resolved-migration.md vault/runbooks/headless-browser-page-verification.md vault/runbooks/headscale-oidc-boot-order.md vault/runbooks/headscale-onboard-node.md vault/runbooks/ia-cli-reference.md vault/runbooks/ia-download-mirror.md vault/runbooks/idahomesh-bridge-setup.md vault/runbooks/idahomesh-vpn-device-setup.md vault/runbooks/lxc-service-migration.md vault/runbooks/mailcow-create-mailbox.md vault/runbooks/meshai-prod-compose-override.md vault/runbooks/meshmonitor-password-reset.md vault/runbooks/meshtastic-sidecar-node.md vault/runbooks/meshtasticd-sim-nodes-runbook.md vault/runbooks/nordvpn-lxc.md vault/runbooks/peertube-remote-runner.md vault/runbooks/pg-backup.md vault/runbooks/pi-nas-omv-runbook.md vault/runbooks/pipeline-patterns.md vault/runbooks/proxmox-create-ubuntu-vm.md vault/runbooks/proxmox-onboard-node.md vault/runbooks/pymc-repeater-kiss-tnc-reenumeration.md vault/runbooks/recon-operations.md vault/runbooks/recon-service-integration.md vault/runbooks/syncthing-add-node.md vault/runbooks/toc-cortex-pve9.2-update.md vault/session-resume/SESSION-HANDOFF-meshai-test.md
2026-07-13 12:00:23 +00:00

5.6 KiB

title type tags aliases related updated
Expose Service on edge2 (Contabo Cloud VPS) runbook
proxmox
lxc-service-migration
expose-service-contabo
expose-service-home
edge2-access-reference
caddy
2026-07-13

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/toc
source /home/zvx/projects/.ref/credentials
godaddy-dns.py add-a <zone> <subdomain> 184.174.35.153

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

# 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.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:

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.