Container Setup & Standards
Principle
Every LXC container is set up identically from a base template. Same tools, same skills, same conventions. A developer can move between containers and know exactly how everything works.
Base Template
All containers were cloned from a template with:
| Package | Purpose |
|---|---|
| Node.js 22 | Runtime for Claude Code and projects |
| Claude Code 2.1.x | CLI agent |
| Docker 29.2.1 | Containerized databases and services |
| tmux 3.4 | Persistent sessions |
| git | Version control |
| gh (GitHub CLI) | GitHub operations |
| mosh | Mobile-friendly SSH |
| build-essential | Compilation tools |
| python3, pip | Python projects |
| jq, curl, wget, rsync | Utilities |
Docker-in-LXC
All containers have Docker installed with full compose support. The LXC host config enables nesting:
lxc.include = /usr/share/lxc/config/nesting.conf
lxc.apparmor.profile = unconfined
lxc.mount.auto = proc:rw sys:rw cgroup:rw
Container User
All containers use the dev user (uid 1001). The dev user has:
- Home directory:
/home/dev - Password-less sudo access
- SSH key-based authentication only
~/binin PATH (for custom scripts)
Account Assignment
| Container | Developer | Account | Plan |
|---|---|---|---|
| chasclaude | Chas | Account 1 | Max |
| infoclaude | Chas (secondary) | Account 2 | Max |
| seanclaude | Sean | Account 3 | Max |
| jazclaude | Jaz | Account 4 | Max |
| manager | Supervisor | Account 5 | Pro |
Claude Code Settings
Each container has ~/.claude/settings.json:
{
"mcpServers": {
"claude-net": {
"command": "node",
"args": ["/home/dev/.claude-net/mcp-server.js"]
}
},
"hooks": {
"Notification": [{
"hooks": [{"type": "command", "command": "/home/dev/.claude/notify-sound.sh"}]
}],
"Stop": [{
"hooks": [{"type": "command", "command": "/home/dev/.claude/notify-sound.sh"}]
}]
}
}
Standard Skills
Every container has these skills in ~/.claude/commands/:
Git Workflow Skills
| Skill | Purpose |
|---|---|
| /commit | Smart git commit — analyzes changes, writes descriptive message, stages selectively, never amends |
| /push | Push to remote with safety checks — sets up tracking, warns on conflicts, never force pushes |
| /pull | Pull from remote with conflict awareness — offers stash if dirty |
| /status | Quick project overview — branch, remote, uncommitted work, recent history |
Session Management Skills
| Skill | Purpose |
|---|---|
| /end-day | Full session wrap-up — updates memory, changelog, handover, DocHub, commits, pushes |
| /change-project | Same as /end-day — use when switching between projects |
| /handover | Same as /end-day — use when handing off to another developer |
All three names (/end-day, /change-project, /handover) run the same workflow:
- Gathers git state (branch, status, log, remote)
- Asks developer for notes and context
- Updates Claude memory files with session patterns/decisions
- Updates CHANGELOG.md
- Writes HANDOVER.md (current state for next session)
- Checks DocHub coverage and updates documentation
- Commits everything (code + docs + memory)
- Pushes to GitHub
Documentation Skills
| Skill | Purpose |
|---|---|
| /onboard-repo | Set up a new project — creates CLAUDE.md, GitHub repo, DocHub documentation |
| /dochub-report | Scan codebase and generate coverage report — shows documented vs missing |
| /dochub-add | Create new DocHub pages (Tier 1 overview + Tier 2 markdown + Tier 3 manifest) |
| /dochub-fix | Fix all documentation gaps found by the latest report |
| /deploy-dochub | Compile TypeScript and restart DocHub on production droplet |
All skills are project-agnostic and work with any git repository.
tmux Auto-Attach with Session Logging
Each container has ~/.tmux-session.sh that runs on login:
#!/bin/bash
SESSION="main"
LOGFILE="/var/log/claude-sessions/$(date +%Y-%m-%d_%H%M%S).log"
if tmux has-session -t $SESSION 2>/dev/null; then
tmux attach -t $SESSION
else
tmux new-session -d -s $SESSION
tmux pipe-pane -t $SESSION "cat >> $LOGFILE"
tmux attach -t $SESSION
fi
This means:
- Login always attaches to the persistent “main” tmux session
- New sessions automatically start logging via
pipe-pane - Logs go to
/var/log/claude-sessions/within each container - Disconnecting (Ctrl+B, D) or losing connection leaves the session running
- Reconnecting picks up exactly where you left off
Claude Net MCP
Each container has Claude Net configured at ~/.claude-net/:
| File | Purpose |
|---|---|
config.json |
Machine name, transport config |
mcp-server.js |
MCP server (patched for localhost) |
Transport uses the host’s SSH tunnel to cms-droplet:
sshHost: "local"— no SSH per requestlocalUrl: "http://10.0.3.1:3500"— hits host bridge → tunnel → hub- Latency: ~95ms (vs ~1.1s with per-request SSH)
Project CLAUDE.md Convention
Every project repo has a CLAUDE.md at the root. This is the AI’s instruction manual:
# Project Name — CLAUDE.md
## What This Project Is
One paragraph description.
## Tech Stack
Languages, frameworks, databases, key dependencies.
## How to Run
Commands to get the project running locally.
## Project Structure
Key directories and files explained.
## Current Work Status
What's been done, what's in progress, what's next.
## Conventions
Coding style, naming, branch naming, commit format.
## Key Decisions
Architecture decisions and WHY. Prevents reopening settled questions.
## Do NOT
Things the AI should never do in this project.
New Developer Onboarding
When a new developer joins:
- Clone LXC container from base template
- Assign static IP and port forwarding on host
- Add DNS A record for
<name>.ipnoelp.io→ 15.204.90.153 - Install Claude Code CLI, authenticate (
claude login) - Set up GitHub auth (
gh auth login) - Copy standard
~/.claude/settings.jsonand skills - Configure Claude Net MCP client
- Set up SSH keys
- Clone project repos
- Install Docker with LXC nesting config
- Test: SSH → tmux → claude → /status
Container-Specific Overrides
While all containers follow the same base template, some have additional services:
chasclaude — Full Dev Environment
Fully provisioned with 8 project repos, Docker databases (CMS Postgres, WIT Postgres, Odoo), Gmail + Resend MCP servers, SSH keys to all production servers, and 9.1 GB of audio files. See the Architecture page for full details.
infoclaude — Production Projects
Hosts lifeonroatan and roatan-news with systemd services and cron jobs. nginx on the OVH5 host proxies news.ipnoelp.com traffic to this container. See the Architecture page for full details.
Permissions
All containers have Claude Code configured to auto-approve non-destructive operations. This eliminates prompt fatigue for routine commands.
Settings in ~/.claude/settings.json:
{
"permissions": {
"allow": [
"Bash(*)",
"Read(*)",
"Edit(*)",
"Write(*)",
"Glob(*)",
"Grep(*)",
"WebFetch(*)",
"WebSearch"
],
"deny": []
}
}
This is deployed on all 5 LXC containers, the laptop, and ser-8.
Claude will only prompt for confirmation on truly destructive actions (force push, rm -rf, etc.) or when it needs clarification on the direction of work.
Session Logging
All tmux sessions are logged to /var/log/claude-sessions/ for later review and analysis.
How It Works
The ~/.tmux-session.sh script runs on every login:
- If a tmux session exists: activates
pipe-panelogging, then attaches - If no session exists: creates one, activates
pipe-panelogging, shows the container banner, then attaches
Every keystroke and output is captured to timestamped log files: /var/log/claude-sessions/YYYY-MM-DD_HHMMSS.log
Key Detail
Logging activates on every attach, not just new sessions. This ensures no session is ever unlogged, even after container restarts or reconnections.
Log Retention
Logs accumulate indefinitely. The manager instance can access logs from all containers via the host for review.
Login Banners (MOTD)
Each container has a distinctive banner in /etc/motd that displays on login and when a new tmux session starts:
┌──────────────────────────────────────────┐
│ CHASCLAUDE · Chas Primary Dev │
│ OVH5 LXC · chas.ipnoelp.io:2211 │
│ Claude Net: chasclaude │
└──────────────────────────────────────────┘
This prevents confusion when multiple terminal windows are open.