DocHub
LXC-based centralized development server for 5 Claude CLI instances

Server Architecture

Problem Statement

Developers are in the Philippines where power outages and internet drops are common. Claude CLI instances running on local laptops means:

  • Power outage = work stops, context lost
  • Internet drop = CLI session dies
  • Developer sick = idle Claude Max seat ($200/mo)
  • Developer quits = all work on their laptop, gone
  • Weekends = 5 Max accounts sitting unused

Solution: Centralized LXC Dev Server

One dedicated OVH server (OVH5) running LXC containers, one per Claude account. Developers SSH in and work inside their container. tmux keeps sessions alive through disconnections.

Why LXC, Not Docker

Docker containers are for running applications. LXC containers are lightweight VMs — full Linux environments with systemd, SSH, and critically: Docker can run natively inside LXC. This matters because Claude CLI regularly needs to spin up databases, services, and tools in Docker as part of normal project work.

Server Details

Item Details
Hostname main
IP 15.204.90.153
SSH ssh ovh5
OS Ubuntu 24.04 LTS
CPU 4 vCPU
RAM 8 GB
Boot disk /dev/sda — 387 GB
Data disk /dev/sdb → /data — 98 GB ext4 (84 GB free)
LXC storage /data/lxc/

Server Layout

OVH5 "main" (4 vCPU / 8GB RAM / 387GB + 98GB data)
|
+-- HOST (ubuntu user)
|   +-- Claude Net SSH tunnel (systemd: claude-net-tunnel)
|   +-- LXC management, NAT, port forwarding
|   +-- /data/lxc/ -- all container rootfs
|   +-- Dynamic IP whitelisting (PAM + fail2ban)
|   +-- container-status.sh monitoring script
|   +-- nginx: news.ipnoelp.com + news.lifeonroatan.com -> 10.0.3.12:8001
|
+-- LXC: chasclaude (10.0.3.11, port 2211)  <- Chas primary
|   +-- Claude CLI (Max account) in tmux
|   +-- All standard skills + Claude Net MCP
|   +-- Docker 29.2.1 with compose
|   +-- 8 project repos cloned + npm installed
|   +-- Postgres containers (CMS, WIT, Odoo)
|   +-- SSH keys to all production servers
|   +-- Gmail + Resend MCP servers configured
|   +-- Mosh: UDP 60011-60020
|
+-- LXC: infoclaude (10.0.3.12, port 2212)  <- Chas secondary
|   +-- Claude CLI (Max account) in tmux
|   +-- All standard skills + Claude Net MCP
|   +-- Docker 29.2.1 with compose
|   +-- Mosh: UDP 60021-60030
|   +-- Projects: lifeonroatan (roatan_scraper), roatan-news (news_system)
|   +-- systemd: roatan-news-api (port 8001)
|   +-- Cron: scraper 3AM, Astro build 3:30AM, news scraper every 30min
|
+-- LXC: seanclaude (10.0.3.13, port 2213)  <- Sean
|   +-- Claude CLI (Max account) in tmux
|   +-- All standard skills + Claude Net MCP
|   +-- Docker 29.2.1 with compose
|   +-- Mosh: UDP 60031-60040
|
+-- LXC: jazclaude (10.0.3.14, port 2214)   <- Jaz
|   +-- Claude CLI (Max account) in tmux
|   +-- All standard skills + Claude Net MCP
|   +-- Docker 29.2.1 with compose
|   +-- Mosh: UDP 60041-60050
|
+-- LXC: manager (10.0.3.15, port 2215)     <- Supervisor (Pro)
    +-- Claude CLI (Pro account) in tmux
    +-- container-status, session-logs monitoring tools
    +-- Docker 29.2.1 with compose
    +-- Mosh: UDP 60051-60060

Note: The container at 10.0.3.12 was renamed from “serclaude” to “infoclaude” on 2026-02-28. The name “serclaude” now refers to Chas’s home server (ser-8 mini), accessible via SSH with Cloudflare tunnel fallback.

DNS Hostnames

All containers are accessible via memorable DNS hostnames on the ipnoelp.io domain (Cloudflare DNS-only, no proxy):

Hostname Container Port
chas.ipnoelp.io chasclaude 2211
info.ipnoelp.io infoclaude 2212
sean1.ipnoelp.io seanclaude 2213
jaz.ipnoelp.io jazclaude 2214
mgr.ipnoelp.io manager 2215

These are configured as A records pointing to 15.204.90.153 with Cloudflare proxy disabled (grey cloud / DNS only) so SSH traffic passes through directly.

Laptop SSH Config

Developers add entries to ~/.ssh/config on their laptops:

Host chasclaude
    HostName chas.ipnoelp.io
    Port 2211
    User dev
    IdentityFile ~/.ssh/ovh5_dev
    IdentitiesOnly yes

Launcher Scripts

Chas has launcher scripts in ~/bin/ on his laptop that use wmctrl to focus existing Kitty terminal windows or open new SSH connections:

Script Target Notes
chasclaude chas.ipnoelp.io:2211 Direct SSH
infoclaude info.ipnoelp.io:2212 Direct SSH
serclaude ser-8 mini Local SSH (3s timeout) with Cloudflare tunnel fallback
lapclaude localhost Local tmux session

Networking

Port Forwarding (in /etc/rc.local)

External Port Container Internal Port Protocol
2211 chasclaude (10.0.3.11) 22 TCP (SSH)
2212 infoclaude (10.0.3.12) 22 TCP (SSH)
2213 seanclaude (10.0.3.13) 22 TCP (SSH)
2214 jazclaude (10.0.3.14) 22 TCP (SSH)
2215 manager (10.0.3.15) 22 TCP (SSH)
60011-60020 chasclaude 60011-60020 UDP (Mosh)
60021-60030 infoclaude 60021-60030 UDP (Mosh)
60031-60040 seanclaude 60031-60040 UDP (Mosh)
60041-60050 jazclaude 60041-60050 UDP (Mosh)
60051-60060 manager 60051-60060 UDP (Mosh)

Network Stack

Component Details
Bridge lxcbr0 (10.0.3.1/24)
Container IPs Static via netplan (10.0.3.11-15)
NAT iptables MASQUERADE in /etc/rc.local
IP forwarding net.ipv4.ip_forward=1
DNS 8.8.8.8, 8.8.4.4 (in container netplan)

Docker in LXC

All containers have Docker 29.2.1 installed with full compose support. This requires LXC nesting configuration on the host:

# Added to each container config in /data/lxc/<name>/config
lxc.include = /usr/share/lxc/config/nesting.conf
lxc.apparmor.profile = unconfined
lxc.mount.auto = proc:rw sys:rw cgroup:rw

Developers use Docker for project databases, services, and tools as part of normal development work.

How Developers Connect

  1. SSH: ssh chasclaude (alias in ~/.ssh/config on laptop)
  2. Mosh: mosh --ssh="ssh -p 2211" dev@chas.ipnoelp.io -p 60011
  3. tmux auto-attach: Login shell runs ~/.tmux-session.sh — creates or reattaches “main” session
  4. Power outage recovery: Pick up phone, SSH app, reattach tmux — Claude still running

Claude Net Optimization

Persistent SSH tunnel on host (systemd claude-net-tunnel):

ssh -N -L 0.0.0.0:3500:127.0.0.1:3500 cms-droplet

Containers hit http://10.0.3.1:3500 (host bridge IP) which tunnels to the hub. Latency: ~95ms (vs ~1.1s with per-request SSH).

Container config uses "sshHost": "local" and "localUrl": "http://10.0.3.1:3500".

Security

Measure Details
SSH keys only Password auth disabled on host and all containers
fail2ban 3 retries -> 24h ban
UFW Only required ports open
Dynamic IP whitelist PAM hook whitelists connecting IP for 3 days
Unattended upgrades Security patches auto-applied
No root SSH Root login disabled

chasclaude: Full Dev Environment

chasclaude is fully provisioned as Chas’s primary development machine:

Projects Cloned

Project Local Dir Status
hypnoelp-cms CMS Full setup (Docker DB, .env, npm, audio)
dochub DocHub Full setup (.env, npm)
wit-proptech Craig Full setup (Docker DB, .env, npm)
captainvans-odoo CaptainVans-Odoo Docker compose ready
elevenlabs-tts ELABS/elevenlabs-v3-tts npm installed
workbook-gen cc npm installed (backend + frontend)
claude-skills ClaudeSkills Cloned
whatsapp-crm-site whatsapp-crm-site rsync’d (not a git repo)

Docker Databases

Container Port Project
cms-db (Postgres) 5433 CMS
wit-db (Postgres) 5434 WIT PropTech
odoo-db + odoo 5435, 8069 Captain Van’s

MCP Servers

Server Auth
Gmail (omelasai) OAuth tokens in ~/.gmail-mcp/
Gmail (lifeonroatan) OAuth tokens in ~/.gmail-mcp-lifeonroatan/
Resend API key in .mcp.json
Claude Net Standard container config

SSH Keys to Production

All server keys from laptop copied: cms_droplet, ovh_vps, ovh2_dev, ovh3_worker, ovh4_vps, hostinger_hypnoelp.

Audio Files

9.1 GB audio directory rsync’d from laptop to ~/code/audio/ (core + course files).

infoclaude: Project Services

infoclaude (formerly serclaude LXC) hosts two production projects:

Projects

Project GitHub Repo Purpose
lifeonroatan OmelasAI/lifeonroatan Roatan business directory scraper and Astro static site
roatan-news OmelasAI/roatan-news Roatan news aggregator with API

Services Inside infoclaude (10.0.3.12)

Service Type Details
roatan-news-api systemd News API on port 8001
roatan_scraper cron cron Business directory scraper, runs daily at 3:00 AM
Astro build+deploy cron cron Builds static site and deploys to Hostinger, runs daily at 3:30 AM
News scraper cron cron News aggregation, runs every 30 minutes

nginx on OVH5 Host

The News API is served via nginx on the OVH5 host. The host nginx reverse-proxies external traffic to the infoclaude container.

Item Details
Config file /etc/nginx/sites-available/news-api
Domains news.ipnoelp.com, news.lifeonroatan.com
Upstream 10.0.3.12:8001 (infoclaude container)

Pending Setup

Task Blocker
claude login on seanclaude, jazclaude, manager Needs browser
gh auth login on seanclaude, jazclaude, manager Needs browser
Send onboarding emails to Sean and Jaz Drafts ready in Gmail
Hourly IP whitelist cleanup cron Not yet configured
LXC snapshots (nightly) Not yet configured
Claude Net v2 (exec, permissions) Future enhancement
Telegram integration Future enhancement