initial scaffold: Meridian LXC (Node 22 + npm @rynfar/meridian + systemd)
Deploys @rynfar/meridian on a Debian 12 LXC, bound to 0.0.0.0:3456. OAuth credentials transferred manually after first deploy (claude login on Mac, scp ~/.claude to /opt/meridian/.claude). systemd unit is enabled but gated on credentials.json existence so the first deploy doesn't crash-loop. LXC has no auth layer — security model is LAN-only reachability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
- name: reload systemd
|
||||
systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: restart meridian
|
||||
systemd:
|
||||
name: meridian
|
||||
state: restarted
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
# ---------- System setup ----------
|
||||
- name: Set timezone
|
||||
copy:
|
||||
content: "{{ timezone }}"
|
||||
dest: /etc/timezone
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Link localtime
|
||||
file:
|
||||
src: "/usr/share/zoneinfo/{{ timezone }}"
|
||||
dest: /etc/localtime
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Install base packages
|
||||
apt:
|
||||
name: "{{ packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: Ensure users exist
|
||||
user:
|
||||
name: "{{ item.name }}"
|
||||
groups: "{{ item.groups }}"
|
||||
shell: "{{ item.shell }}"
|
||||
append: true
|
||||
loop: "{{ users }}"
|
||||
|
||||
- name: Authorize SSH keys for cbalders
|
||||
authorized_key:
|
||||
user: cbalders
|
||||
key: "{{ item }}"
|
||||
state: present
|
||||
loop: "{{ ssh_authorized_keys }}"
|
||||
|
||||
- name: Passwordless sudo for cbalders
|
||||
copy:
|
||||
content: "cbalders ALL=(ALL) NOPASSWD:ALL\n"
|
||||
dest: /etc/sudoers.d/90-cbalders
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0440'
|
||||
|
||||
# ---------- Node 22 (NodeSource apt repo) ----------
|
||||
- name: Ensure /etc/apt/keyrings exists
|
||||
file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Add NodeSource GPG key
|
||||
get_url:
|
||||
url: https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key
|
||||
dest: /etc/apt/keyrings/nodesource.asc
|
||||
mode: '0644'
|
||||
|
||||
- name: Add NodeSource apt repo
|
||||
copy:
|
||||
content: "deb [signed-by=/etc/apt/keyrings/nodesource.asc] https://deb.nodesource.com/node_{{ meridian_node_major }}.x nodistro main\n"
|
||||
dest: /etc/apt/sources.list.d/nodesource.list
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
register: nodesource_repo
|
||||
|
||||
- name: apt update after NodeSource add
|
||||
apt:
|
||||
update_cache: true
|
||||
when: nodesource_repo.changed
|
||||
|
||||
- name: Install Node.js
|
||||
apt:
|
||||
name: nodejs
|
||||
state: present
|
||||
|
||||
- name: Verify Node major version
|
||||
command: node --version
|
||||
register: node_version
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if Node major mismatch
|
||||
fail:
|
||||
msg: "Node {{ node_version.stdout }} installed; expected v{{ meridian_node_major }}.x"
|
||||
when: not node_version.stdout.startswith("v" ~ meridian_node_major | string ~ ".")
|
||||
|
||||
# ---------- Meridian user + home ----------
|
||||
- name: Ensure meridian system user
|
||||
user:
|
||||
name: "{{ meridian_user }}"
|
||||
system: true
|
||||
home: "{{ meridian_home }}"
|
||||
shell: /usr/sbin/nologin
|
||||
create_home: true
|
||||
state: present
|
||||
|
||||
- name: Ensure meridian home perms
|
||||
file:
|
||||
path: "{{ meridian_home }}"
|
||||
state: directory
|
||||
owner: "{{ meridian_user }}"
|
||||
group: "{{ meridian_user }}"
|
||||
mode: '0750'
|
||||
|
||||
- name: Ensure .claude credentials dir
|
||||
file:
|
||||
path: "{{ meridian_home }}/.claude"
|
||||
state: directory
|
||||
owner: "{{ meridian_user }}"
|
||||
group: "{{ meridian_user }}"
|
||||
mode: '0700'
|
||||
|
||||
# ---------- Install Meridian via npm ----------
|
||||
- name: Install @rynfar/meridian globally
|
||||
npm:
|
||||
name: "@rynfar/meridian"
|
||||
global: true
|
||||
state: latest
|
||||
register: meridian_install
|
||||
|
||||
- name: Resolve meridian binary path
|
||||
command: which meridian
|
||||
register: meridian_bin
|
||||
changed_when: false
|
||||
|
||||
# ---------- systemd unit ----------
|
||||
- name: Deploy meridian systemd unit
|
||||
template:
|
||||
src: meridian.service.j2
|
||||
dest: /etc/systemd/system/meridian.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: reload systemd
|
||||
|
||||
- name: Flush handlers (reload systemd before enable)
|
||||
meta: flush_handlers
|
||||
|
||||
- name: Enable meridian service
|
||||
systemd:
|
||||
name: meridian
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
|
||||
# OAuth creds (~/.claude/) are scp'd in manually after first deploy.
|
||||
# Start the service only if creds are present — otherwise the SDK would
|
||||
# crash-loop on missing credentials.
|
||||
- name: Check for Claude OAuth credentials
|
||||
stat:
|
||||
path: "{{ meridian_home }}/.claude/.credentials.json"
|
||||
register: claude_creds
|
||||
|
||||
- name: Start meridian if credentials present
|
||||
systemd:
|
||||
name: meridian
|
||||
state: started
|
||||
when: claude_creds.stat.exists
|
||||
|
||||
- name: Bootstrap reminder
|
||||
debug:
|
||||
msg: |
|
||||
Meridian installed at {{ meridian_bin.stdout }}.
|
||||
OAuth credentials not yet present at {{ meridian_home }}/.claude/.credentials.json.
|
||||
Bootstrap on your Mac:
|
||||
npm i -g @anthropic-ai/claude-code && claude login
|
||||
Then transfer creds:
|
||||
scp -r ~/.claude cbalders@{{ inventory_hostname }}:/tmp/.claude-bootstrap
|
||||
On the LXC:
|
||||
sudo cp -r /tmp/.claude-bootstrap/. {{ meridian_home }}/.claude/
|
||||
sudo chown -R {{ meridian_user }}:{{ meridian_user }} {{ meridian_home }}/.claude/
|
||||
sudo systemctl start meridian
|
||||
when: not claude_creds.stat.exists
|
||||
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=Meridian (Anthropic API → Claude Code SDK bridge)
|
||||
Documentation=https://github.com/rynfar/meridian
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={{ meridian_user }}
|
||||
Group={{ meridian_user }}
|
||||
WorkingDirectory={{ meridian_home }}
|
||||
Environment=HOME={{ meridian_home }}
|
||||
Environment=MERIDIAN_HOST={{ meridian_host }}
|
||||
Environment=MERIDIAN_PORT={{ meridian_port }}
|
||||
Environment=MERIDIAN_IDLE_TIMEOUT_SECONDS={{ meridian_idle_timeout_seconds }}
|
||||
ExecStart={{ meridian_bin.stdout }}
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: restart node_exporter
|
||||
systemd:
|
||||
name: node_exporter
|
||||
state: restarted
|
||||
daemon_reload: yes
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
- name: Check if node_exporter is installed
|
||||
stat:
|
||||
path: /usr/local/bin/node_exporter
|
||||
register: ne_bin
|
||||
|
||||
- name: Set architecture
|
||||
set_fact:
|
||||
ne_arch: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}"
|
||||
|
||||
- name: Get latest node_exporter version
|
||||
uri:
|
||||
url: https://api.github.com/repos/prometheus/node_exporter/releases/latest
|
||||
return_content: yes
|
||||
register: ne_release
|
||||
when: not ne_bin.stat.exists
|
||||
|
||||
- name: Set node_exporter version
|
||||
set_fact:
|
||||
ne_version: "{{ ne_release.json.tag_name | regex_replace('^v', '') }}"
|
||||
when: not ne_bin.stat.exists
|
||||
|
||||
- name: Download node_exporter
|
||||
get_url:
|
||||
url: "https://github.com/prometheus/node_exporter/releases/download/v{{ ne_version }}/node_exporter-{{ ne_version }}.linux-{{ ne_arch }}.tar.gz"
|
||||
dest: /tmp/node_exporter.tar.gz
|
||||
when: not ne_bin.stat.exists
|
||||
|
||||
- name: Extract node_exporter
|
||||
unarchive:
|
||||
src: /tmp/node_exporter.tar.gz
|
||||
dest: /tmp/
|
||||
remote_src: yes
|
||||
when: not ne_bin.stat.exists
|
||||
|
||||
- name: Install node_exporter binary
|
||||
copy:
|
||||
src: "/tmp/node_exporter-{{ ne_version }}.linux-{{ ne_arch }}/node_exporter"
|
||||
dest: /usr/local/bin/node_exporter
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
remote_src: yes
|
||||
when: not ne_bin.stat.exists
|
||||
notify: restart node_exporter
|
||||
|
||||
- name: Create node_exporter user
|
||||
user:
|
||||
name: node_exporter
|
||||
system: yes
|
||||
shell: /usr/sbin/nologin
|
||||
create_home: no
|
||||
|
||||
- name: Deploy node_exporter systemd service
|
||||
copy:
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Prometheus Node Exporter
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=node_exporter
|
||||
Group=node_exporter
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/node_exporter --collector.textfile.directory=/var/lib/node_exporter/textfile
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
dest: /etc/systemd/system/node_exporter.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: restart node_exporter
|
||||
|
||||
- name: Create textfile collector directory
|
||||
file:
|
||||
path: /var/lib/node_exporter/textfile
|
||||
state: directory
|
||||
owner: node_exporter
|
||||
group: node_exporter
|
||||
mode: '0755'
|
||||
|
||||
- name: Deploy CPU temperature collector script
|
||||
copy:
|
||||
content: |
|
||||
#!/bin/bash
|
||||
TEMP=$(vcgencmd measure_temp 2>/dev/null | grep -oP '[0-9.]+')
|
||||
if [ -n "$TEMP" ]; then
|
||||
echo "# HELP node_cpu_temperature_celsius CPU temperature from vcgencmd"
|
||||
echo "# TYPE node_cpu_temperature_celsius gauge"
|
||||
echo "node_cpu_temperature_celsius $TEMP"
|
||||
fi > /var/lib/node_exporter/textfile/cpu_temp.prom
|
||||
dest: /usr/local/bin/collect-cpu-temp.sh
|
||||
mode: '0755'
|
||||
|
||||
- name: Schedule CPU temperature collection (every minute)
|
||||
cron:
|
||||
name: "node_exporter cpu temp"
|
||||
user: node_exporter
|
||||
job: "/usr/local/bin/collect-cpu-temp.sh"
|
||||
|
||||
- name: Run initial temperature collection
|
||||
command: /usr/local/bin/collect-cpu-temp.sh
|
||||
changed_when: false
|
||||
|
||||
- name: Enable and start node_exporter
|
||||
systemd:
|
||||
name: node_exporter
|
||||
enabled: yes
|
||||
state: started
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Clean up download
|
||||
file:
|
||||
path: /tmp/node_exporter.tar.gz
|
||||
state: absent
|
||||
when: not ne_bin.stat.exists
|
||||
Reference in New Issue
Block a user