claude-resources/skills/lxc-service-deployment/SKILL.md
Standardized deployment procedure for services in LXC containers on the Proxmox HA cluster. Use when deploying a new service, creating LXC containers, configuring DHCP reservations (Kea), DNS rewrites (AdGuard), reverse proxy routes (Traefik), or integrating with monitoring (Uptime Kuma) and dashboard (Homepage). Triggers on phrases like "deploy a new service", "create container for", "set up DHCP", "add Traefik route", "configure DNS rewrite", or any homelab infrastructure deployment.
npx skillsauth add festion/homelab-gitops lxc-service-deploymentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Deploy services to the Proxmox HA cluster with full infrastructure integration.
Update these values if infrastructure changes. IPs are used for core infrastructure (DNS may be unavailable during configuration). DNS names are used for validation and service access.
# === CORE INFRASTRUCTURE (use IPs - DNS may not be available) ===
PROXMOX_NODES=("192.168.1.137" "192.168.1.125" "192.168.1.126")
KEA_PRIMARY="192.168.1.133"
KEA_SECONDARY="192.168.1.134"
ADGUARD_PRIMARY="192.168.1.253"
ADGUARD_SECONDARY="192.168.1.224"
TRAEFIK_IP="192.168.1.110"
HOMEPAGE_IP="192.168.1.45"
# === SERVICE ACCESS (use DNS after deployment) ===
TRAEFIK_DASHBOARD="https://traefik.internal.lakehouse.wtf"
UPTIME_KUMA="https://uptime.internal.lakehouse.wtf"
DOMAIN_SUFFIX="internal.lakehouse.wtf"
# === CONFIGURATION PATHS ===
KEA_CONFIG="/etc/kea/kea-dhcp4.conf"
ADGUARD_CONFIG="/opt/AdGuardHome/AdGuardHome.yaml"
TRAEFIK_ROUTERS="/etc/traefik/dynamic/routers.yml"
TRAEFIK_SERVICES="/etc/traefik/dynamic/services.yml"
HOMEPAGE_CONFIG="/home/homepage/homepage/config/services.yaml"
| Component | Primary | Secondary | Config Path |
|-----------|---------|-----------|-------------|
| Proxmox | .137 | .125, .126 | - |
| Kea DHCP | .133 | .134 | /etc/kea/kea-dhcp4.conf |
| AdGuard DNS | .253 | .224 | /opt/AdGuardHome/AdGuardHome.yaml |
| Traefik | .110 | - | /etc/traefik/dynamic/*.yml |
| Uptime Kuma | .132 | - | Web UI |
| Homepage | .45 | - | /home/homepage/homepage/config/services.yaml |
Domain Pattern: <service>.internal.lakehouse.wtf → Traefik → Backend
┌─────────────────────────────────────────────────────────────┐
│ External DNS (Cloudflare) │
│ *.lakehouse.wtf → Cloudflare (tunnel/proxy) │
│ *.internal.lakehouse.wtf → Cloudflare (needs override!) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Internal DNS (AdGuard Rewrite) - OVERRIDES Cloudflare │
│ service.internal.lakehouse.wtf → Traefik IP │
│ (Per-service rewrites for flexibility with standalone HW) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Traefik Reverse Proxy │
│ Routes by Host header → Backend service IP:PORT │
│ TLS termination with Cloudflare wildcard cert │
└─────────────────────────────────────────────────────────────┘
Why per-service DNS rewrites (not wildcard):
Gather this information before starting:
| Field | Value | Notes |
|-------|-------|-------|
| Service name | | Lowercase, hyphenated (e.g., my-service) |
| LXC ID | | Check availability across cluster |
| Target IP | | Verify not in use |
| Service port | | Check app documentation |
| Health check path | | Usually /, /health, or /api/health |
| CPU cores | | Default: 2 |
| RAM (MB) | | Default: 2048 |
| Disk (GB) | | Default: 20 |
Verify availability:
SERVICE="my-service"
IP="192.168.1.XXX"
LXC_ID="XXX"
# Check IP not in use
ping -c 2 $IP
# Check LXC ID available across cluster
for node in "${PROXMOX_NODES[@]}"; do
ssh root@$node "pct list | grep -w $LXC_ID" && echo "⚠️ ID $LXC_ID in use on $node"
done
NODE="${PROXMOX_NODES[0]}" # Or select based on resource availability
LXC_ID="XXX"
HOSTNAME="service-name"
IP="192.168.1.XXX"
# Create container (Debian 12)
ssh root@$NODE "pct create $LXC_ID local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname $HOSTNAME \
--memory 2048 --cores 2 --rootfs local-lvm:20 \
--net0 name=eth0,bridge=vmbr0,ip=$IP/24,gw=192.168.1.1 \
--onboot 1 --unprivileged 1"
ssh root@$NODE "pct start $LXC_ID"
# Get MAC address (needed for DHCP)
MAC=$(ssh root@$NODE "pct config $LXC_ID | grep hwaddr | awk '{print \$2}'")
echo "MAC Address: $MAC"
⚠️ Both servers must be updated for HA failover:
SERVICE="service-name"
IP="192.168.1.XXX"
MAC="XX:XX:XX:XX:XX:XX"
# Backup configs on both servers
ssh root@$KEA_PRIMARY "cp $KEA_CONFIG ${KEA_CONFIG}.backup-$(date +%Y%m%d)"
ssh root@$KEA_SECONDARY "cp $KEA_CONFIG ${KEA_CONFIG}.backup-$(date +%Y%m%d)"
# Add reservation to primary (edit the 192.168.1.0/24 subnet's reservations array)
# Format: {"hw-address": "$MAC", "ip-address": "$IP", "hostname": "$SERVICE"}
# Copy config to secondary
scp root@$KEA_PRIMARY:$KEA_CONFIG /tmp/kea-dhcp4.conf
scp /tmp/kea-dhcp4.conf root@$KEA_SECONDARY:$KEA_CONFIG
# Reload both servers
ssh root@$KEA_PRIMARY "systemctl reload isc-kea-dhcp4-server"
ssh root@$KEA_SECONDARY "systemctl reload isc-kea-dhcp4-server"
# Verify
ssh root@$KEA_PRIMARY "grep -A2 '$MAC' $KEA_CONFIG"
⚠️ Both servers must be updated for HA failover:
SERVICE="service-name"
DOMAIN="${SERVICE}.${DOMAIN_SUFFIX}"
# Backup and add rewrite to BOTH AdGuard servers
for AG in $ADGUARD_PRIMARY $ADGUARD_SECONDARY; do
ssh root@$AG "cp $ADGUARD_CONFIG ${ADGUARD_CONFIG}.backup-$(date +%Y%m%d)"
# Add rewrite entry (append to rewrites section)
ssh root@$AG "cat >> $ADGUARD_CONFIG << EOF
- domain: $DOMAIN
answer: $TRAEFIK_IP
EOF"
ssh root@$AG "systemctl restart AdGuardHome"
done
# Verify DNS resolution (should return Traefik IP)
echo "Testing DNS resolution..."
dig +short $DOMAIN @$ADGUARD_PRIMARY
dig +short $DOMAIN @$ADGUARD_SECONDARY
Note on standalone hardware: If deploying to hardware outside Proxmox that should bypass Traefik, point the DNS rewrite directly to the hardware IP instead of Traefik.
Add router to $TRAEFIK_ROUTERS:
service-name-router:
rule: "Host(`service-name.internal.lakehouse.wtf`)"
entryPoints:
- websecure
service: service-name-service
tls:
certResolver: cloudflare
Add service to $TRAEFIK_SERVICES:
service-name-service:
loadBalancer:
servers:
- url: "http://192.168.1.XXX:PORT"
healthCheck:
path: /health
interval: 30s
timeout: 5s
# Apply changes
ssh root@$TRAEFIK_IP "systemctl reload traefik"
# Verify via API (uses DNS - confirms full chain works)
curl -sk "$TRAEFIK_DASHBOARD/api/http/routers" | grep -q "service-name" && echo "✅ Router configured"
curl -sk "$TRAEFIK_DASHBOARD/api/http/services" | grep -q "service-name" && echo "✅ Service configured"
Uptime Kuma (access via $UPTIME_KUMA):
https://service-name.internal.lakehouse.wtfHomepage (edit on $HOMEPAGE_IP):
ssh root@$HOMEPAGE_IP "cat >> $HOMEPAGE_CONFIG << 'EOF'
- Service Name:
icon: service-icon
href: https://service-name.internal.lakehouse.wtf
description: Service description
EOF"
Run all checks - all should pass before marking deployment complete:
SERVICE="service-name"
IP="192.168.1.XXX"
PORT="XXXX"
MAC="XX:XX:XX:XX:XX:XX"
DOMAIN="${SERVICE}.${DOMAIN_SUFFIX}"
echo "=== Deployment Validation ==="
# 1. Container accessible
ssh root@$IP "hostname" &>/dev/null && echo "✅ Container accessible via SSH" || echo "❌ Container SSH failed"
# 2. Service listening on expected port
ssh root@$IP "ss -tlnp | grep -q ':$PORT'" && echo "✅ Service listening on port $PORT" || echo "❌ Service not listening"
# 3. DNS resolving to Traefik
dig +short $DOMAIN | grep -q "$TRAEFIK_IP" && echo "✅ DNS resolves to Traefik" || echo "❌ DNS misconfigured"
# 4. HTTPS accessible through Traefik
curl -skI "https://$DOMAIN" | grep -qE "HTTP/[12]" && echo "✅ HTTPS working via Traefik" || echo "❌ HTTPS failed"
# 5. DHCP reservation exists
ssh root@$KEA_PRIMARY "grep -q '$MAC' $KEA_CONFIG" && echo "✅ DHCP reservation on primary" || echo "❌ DHCP missing on primary"
ssh root@$KEA_SECONDARY "grep -q '$MAC' $KEA_CONFIG" && echo "✅ DHCP reservation on secondary" || echo "❌ DHCP missing on secondary"
# 6. Traefik health check passing
curl -sk "$TRAEFIK_DASHBOARD/api/http/services" | grep -A5 "$SERVICE" | grep -q '"status":"UP"' && echo "✅ Traefik health check passing" || echo "⚠️ Check Traefik health status"
echo "=== Validation Complete ==="
For services running on dedicated hardware (not LXC), skip Phase 2 (container creation) and adjust:
| Issue | Diagnosis | Solution |
|-------|-----------|----------|
| 502 Bad Gateway | curl -I http://$IP:$PORT fails | Service not running or wrong port in Traefik |
| DNS returns Cloudflare IP | dig +short $DOMAIN shows 104.x.x.x | AdGuard rewrite missing - add to both servers |
| IP Conflict | ping $IP responds before container created | Choose different IP |
| DHCP not assigning IP | Container gets random IP | Check MAC in reservation matches pct config |
| Traefik 404 | Route exists but 404 | Check Host() rule matches exactly |
# 1. Stop and destroy container
ssh root@$NODE "pct stop $LXC_ID && pct destroy $LXC_ID"
# 2. Remove DHCP reservation (restore backup or edit manually)
ssh root@$KEA_PRIMARY "cp ${KEA_CONFIG}.backup-* $KEA_CONFIG && systemctl reload isc-kea-dhcp4-server"
ssh root@$KEA_SECONDARY "cp ${KEA_CONFIG}.backup-* $KEA_CONFIG && systemctl reload isc-kea-dhcp4-server"
# 3. Remove Traefik config (edit files to remove router/service entries)
ssh root@$TRAEFIK_IP "systemctl reload traefik"
# 4. Remove DNS rewrite (restore backup or edit manually)
ssh root@$ADGUARD_PRIMARY "cp ${ADGUARD_CONFIG}.backup-* $ADGUARD_CONFIG && systemctl restart AdGuardHome"
ssh root@$ADGUARD_SECONDARY "cp ${ADGUARD_CONFIG}.backup-* $ADGUARD_CONFIG && systemctl restart AdGuardHome"
# 5. Remove from Uptime Kuma (manual via web UI)
# 6. Remove from Homepage (edit $HOMEPAGE_CONFIG)
tools
TP-Link Omada SDN network management for the Lakehouse homelab. Use when managing switches, access points, VLANs, spanning tree, or WiFi infrastructure. Triggers on phrases like "adopt device", "configure VLAN", "switch CLI", "WiFi troubleshooting", "Omada API", "STP blocking", "PoE budget", or any network hardware management.
testing
Print settings and optimization for Prusa Mini+ with E3D Revo Micro hotend, bowden direct drive extruder, and ObXidian nozzles (0.25mm, 0.4mm, 0.6mm, 0.8mm). Use when the user asks about print settings, slicer profiles, troubleshooting prints, material temperatures, flow rates, nozzle selection, or optimizing prints for this specific printer configuration. Triggers on phrases like "print settings", "slicer profile", "layer height", "print speed", "temperature", "first layer", "stringing", "calibration", "which nozzle", or any 3D printing task mentioning Prusa Mini, Revo, or ObXidian.
data-ai
Use when working with 3D model files (STL), print tracking, slicer profiles, or managing the model-catalog homelab service at model-catalog.internal.lakehouse.wtf
tools
Create and edit 3D printable mesh files (STL, 3MF, OBJ) programmatically using Python trimesh + manifold3d. Use when the user asks to create 3D objects, modify existing STL/3MF files, add holes/features to meshes, combine or split meshes, repair meshes, convert between formats, analyze mesh geometry, or generate parametric parts for 3D printing. Triggers on "create STL", "make a 3D model", "edit mesh", "add hole", "modify STL", "3MF", "printable", "mesh repair", "boolean", "combine meshes", or any 3D modeling task that doesn't require a GUI CAD tool.