4ab85f02279b65949f1990737d5f353d3a98676f
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
homelab-ansible-lxc-meridian
Ansible config for the Meridian LXC (CTID 457 on pve01, 192.168.1.164).
What it is
Meridian is a local Anthropic API server
backed by the Claude Code SDK. It translates /v1/messages calls into Claude
Code SDK query() calls so any Anthropic-compatible client can run against
Chuck's Claude Max subscription instead of paid API tokens.
Primary consumer: HAOS's built-in anthropic conversation integration (via a
custom_component fork that adds CONF_BASE_URL).
Architecture
- Debian 12 LXC, Node 22 from NodeSource apt repo
@rynfar/meridianinstalled globally vianpm- systemd unit
meridian.servicerunning as usermeridian, binding to0.0.0.0:3456 - OAuth credentials live at
/opt/meridian/.claude/(transferred manually after first deploy — see Bootstrap below) - No auth at the Meridian layer. LAN-only reachability is the entire security model — no Caddy public vhost, no Cloudflare tunnel.
Bootstrap
- Provision the LXC via
homelab-terraform/lxc(terraform apply). - Run the LXC bootstrap one-liner from
feedback_lxc_bootstrap_user:(Plus authorized_keys for cbalders.)ssh root@192.168.1.164 'apt-get update && apt-get install -y sudo && useradd -m -s /bin/bash cbalders && echo "cbalders ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/90-cbalders && chmod 440 /etc/sudoers.d/90-cbalders' - Local first deploy (Semaphore can't reach a fresh host):
Expect: Node 22 installed,
./deploy.sh@rynfar/meridianinstalled, systemd unit deployed and enabled but not started (no creds yet —claude_creds.stat.existsgates the start task). - OAuth bootstrap — run
claude auth login --claudeaidirectly on the LXC via the bundled binary. Do not scp~/.claude/from your Mac — macOS keeps the refresh token in the Keychain and the snapshot 401s as soon as the short-lived access token expires (incident write-up: 2026-05-17 → 2026-05-19, see project_meridian).# Stop the service so it's not racing the auth writer. ssh cbalders@192.168.1.164 sudo systemctl stop meridian # Paste-code flow as the meridian user (needs -t for TTY). ssh -t cbalders@192.168.1.164 \ 'sudo -u meridian -H /usr/lib/node_modules/@rynfar/meridian/node_modules/@anthropic-ai/claude-code/bin/claude.exe auth login --claudeai' # → prints https://claude.com/cai/oauth/authorize?... — paste into a Mac # browser, log in with the Max account, paste the code back. # → ends with: Login successful. ssh cbalders@192.168.1.164 sudo systemctl start meridian # Verify (expect loggedIn: true, subscriptionType: max): ssh cbalders@192.168.1.164 \ 'sudo -u meridian -H /usr/lib/node_modules/@rynfar/meridian/node_modules/@anthropic-ai/claude-code/bin/claude.exe auth status' - Smoke from a LAN host:
curl http://192.168.1.164:3456/v1/messages \ -H 'Content-Type: application/json' \ -H 'anthropic-version: 2023-06-01' \ -d '{"model":"claude-haiku-4-5","max_tokens":40,"messages":[{"role":"user","content":"reply with the single word: pong"}]}'
Operations
- Subsequent deploys: via Semaphore template "Meridian Deploy" (added to the sync-semaphore-state.py manifest).
- Token refresh: handled automatically by the Claude Code SDK; if it ever
fails,
sudo -u meridian /usr/bin/meridian refresh-tokenfrom the LXC. - Restart after creds change:
sudo systemctl restart meridian. - Logs:
journalctl -u meridian -f.
Files
roles/meridian/ Node 22 install + npm i meridian + systemd unit
roles/node_exporter/ Prometheus exporter for fleet metrics
vars/main.yml base packages, ssh keys, meridian config
site.yml playbook entrypoint
inventory.ini single host (192.168.1.164)
deploy.sh wrapper for local first-run
Memory pointers
project_meridian— overall design, OAuth model, consumersfeedback_local_dns_only— DNS convention (no public CF for services)feedback_lxc_bootstrap_user— root bootstrap pattern for fresh LXCsfeedback_fresh_host_bootstrap— Semaphore can't reach fresh hosts
Description
Languages
Jinja
68.7%
Shell
31.3%