phoenixd upgrade

This commit is contained in:
counterweight 2026-08-08 15:19:11 +02:00
parent ba4ba504d0
commit d0bfc2650d
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
5 changed files with 795 additions and 5 deletions

View file

@ -0,0 +1,535 @@
---
# phoenixd Deployment Playbook
#
# Deploys phoenixd (https://phoenix.acinq.co/server), the server version of the
# Phoenix Lightning wallet, on vipy so LNBits can use it as a wallet backend
# over loopback.
#
# What this does:
# 1. Downloads the pinned phoenixd release and installs phoenixd + phoenix-cli
# 2. Creates a dedicated system user and a 0700 data directory
# 3. Creates and enables a systemd service
# 4. Creates a push-monitor health check script + systemd timer
# 5. Registers a push monitor in Uptime Kuma
#
# The HTTP API stays bound to 127.0.0.1 and is NOT proxied by Caddy: phoenixd
# holds funds and its API is protected by a single password. Anything that needs
# it either runs on this host or reaches it over the Tailscale mesh.
#
# ⚠️ After the first run, back up {{ phoenixd_data_dir }}/seed.dat. Losing it
# means losing the funds. See setup_backup_phoenixd_to_lapy.yml.
- name: Deploy phoenixd on vipy
hosts: vipy
become: yes
vars_files:
- ../../infra_vars.yml
- ../../services_config.yml
- ../../infra_secrets.yml
- ./phoenixd_vars.yml
vars:
uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}"
tasks:
# ===========================================
# Prerequisites
# ===========================================
- name: Install phoenixd runtime dependencies
apt:
name:
- unzip
- curl
state: present
update_cache: yes
# ===========================================
# System User and Directories
# ===========================================
- name: Create phoenixd system group
group:
name: "{{ phoenixd_group }}"
system: yes
- name: Create phoenixd system user
user:
name: "{{ phoenixd_user }}"
group: "{{ phoenixd_group }}"
system: yes
shell: /usr/sbin/nologin
home: "{{ phoenixd_home }}"
create_home: yes
comment: "phoenixd Lightning node"
- name: Create phoenixd home directory
file:
path: "{{ phoenixd_home }}"
state: directory
owner: "{{ phoenixd_user }}"
group: "{{ phoenixd_group }}"
mode: "0750"
- name: Create phoenixd data directory
file:
path: "{{ phoenixd_data_dir }}"
state: directory
owner: "{{ phoenixd_user }}"
group: "{{ phoenixd_group }}"
mode: "0700"
# ===========================================
# Download and Install
# ===========================================
- name: Check if phoenixd is already installed
stat:
path: "{{ phoenixd_bin_dir }}/phoenixd"
register: phoenixd_binary
- name: Check installed phoenixd version
command: "{{ phoenixd_bin_dir }}/phoenixd --version"
register: phoenixd_installed_version
changed_when: false
failed_when: false
when: phoenixd_binary.stat.exists
- name: Decide whether phoenixd needs installing
set_fact:
phoenixd_needs_install: >-
{{ not phoenixd_binary.stat.exists
or phoenixd_version not in (phoenixd_installed_version.stdout | default('')) }}
- name: Download phoenixd {{ phoenixd_version }}
get_url:
url: "{{ phoenixd_url }}"
dest: "/tmp/phoenixd-{{ phoenixd_version }}.zip"
mode: "0644"
when: phoenixd_needs_install | bool
- name: Create temporary extraction directory
file:
path: /tmp/phoenixd-extract
state: directory
mode: "0755"
when: phoenixd_needs_install | bool
- name: Extract phoenixd archive
unarchive:
src: "/tmp/phoenixd-{{ phoenixd_version }}.zip"
dest: /tmp/phoenixd-extract
remote_src: yes
when: phoenixd_needs_install | bool
- name: Locate extracted binaries
find:
paths: /tmp/phoenixd-extract
patterns: "{{ item }}"
recurse: yes
file_type: file
register: phoenixd_extracted
loop:
- phoenixd
- phoenix-cli
when: phoenixd_needs_install | bool
- name: Fail if the archive did not contain the expected binaries
assert:
that:
- item.files | length > 0
fail_msg: "Could not find '{{ item.item }}' in the phoenixd {{ phoenixd_version }} archive"
loop: "{{ phoenixd_extracted.results }}"
loop_control:
label: "{{ item.item }}"
when: phoenixd_needs_install | bool
- name: Install phoenixd and phoenix-cli binaries
copy:
src: "{{ item.files[0].path }}"
dest: "{{ phoenixd_bin_dir }}/{{ item.item }}"
remote_src: yes
owner: root
group: root
mode: "0755"
loop: "{{ phoenixd_extracted.results }}"
loop_control:
label: "{{ item.item }}"
when: phoenixd_needs_install | bool
notify: Restart phoenixd
- name: Clean up phoenixd download artifacts
file:
path: "{{ item }}"
state: absent
loop:
- "/tmp/phoenixd-{{ phoenixd_version }}.zip"
- /tmp/phoenixd-extract
# ===========================================
# Systemd Service
# ===========================================
- name: Build phoenixd command line arguments
set_fact:
phoenixd_args: >-
{{ (['--agree-to-terms-of-service'] if phoenixd_agree_tos else [])
+ ['--chain', phoenixd_chain]
+ ['--auto-liquidity', phoenixd_auto_liquidity]
+ ['--http-bind-ip', phoenixd_http_bind_ip]
+ ['--http-bind-port', phoenixd_http_bind_port | string]
+ (['--max-mining-fee', phoenixd_max_mining_fee | string] if phoenixd_max_mining_fee else [])
+ (['--webhook', phoenixd_webhook_url] if phoenixd_webhook_url else [])
+ ['--silent'] }}
- name: Create phoenixd systemd service
copy:
dest: /etc/systemd/system/phoenixd.service
content: |
[Unit]
Description=phoenixd - Lightning Network Node
Documentation=https://phoenix.acinq.co/server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ phoenixd_user }}
Group={{ phoenixd_group }}
WorkingDirectory={{ phoenixd_home }}
Environment=PHOENIX_DATADIR={{ phoenixd_data_dir }}
ExecStart={{ phoenixd_bin_dir }}/phoenixd {{ phoenixd_args | join(' ') }}
Restart=always
RestartSec=30
TimeoutStartSec=120
TimeoutStopSec=120
StandardOutput=journal
StandardError=journal
# Hardening: the node only ever writes to its own data directory
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths={{ phoenixd_data_dir }}
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
owner: root
group: root
mode: "0644"
notify: Restart phoenixd
- name: Reload systemd daemon
systemd:
daemon_reload: yes
- name: Enable and start phoenixd
systemd:
name: phoenixd
enabled: yes
state: started
- name: Flush handlers so phoenixd is running before we inspect its data dir
meta: flush_handlers
# ===========================================
# First Boot Checks
# ===========================================
- name: Wait for phoenixd to write its config file
wait_for:
path: "{{ phoenixd_data_dir }}/phoenix.conf"
state: present
timeout: 120
- name: Check that the seed file exists
stat:
path: "{{ phoenixd_data_dir }}/seed.dat"
register: phoenixd_seed_file
- name: Fail if phoenixd did not create a seed
assert:
that:
- phoenixd_seed_file.stat.exists
fail_msg: "phoenixd started but {{ phoenixd_data_dir }}/seed.dat is missing - check 'journalctl -u phoenixd'"
# ===========================================
# Health Check Script + Systemd Timer
# ===========================================
- name: Create phoenixd health check script
copy:
dest: "{{ phoenixd_healthcheck_script_path }}"
content: |
#!/bin/bash
# Checks phoenixd and pushes the result to Uptime Kuma.
UPTIME_KUMA_PUSH_URL="${UPTIME_KUMA_PUSH_URL}"
export PHOENIX_DATADIR="{{ phoenixd_data_dir }}"
check_phoenixd() {
# Service must be active and the node must answer getinfo.
# phoenix-cli reads the api password from $PHOENIX_DATADIR/phoenix.conf,
# but not the bind address, so pass it explicitly.
systemctl is-active --quiet phoenixd && \
{{ phoenixd_bin_dir }}/phoenix-cli \
--http-bind-ip {{ phoenixd_http_bind_ip }} \
--http-bind-port {{ phoenixd_http_bind_port }} \
getinfo 2>/dev/null | grep -q '"nodeId"'
}
push_to_uptime_kuma() {
local status=$1
local msg=$2
if [ -z "$UPTIME_KUMA_PUSH_URL" ]; then
echo "ERROR: UPTIME_KUMA_PUSH_URL not set"
return 1
fi
curl -s --max-time 10 --retry 2 -o /dev/null \
"${UPTIME_KUMA_PUSH_URL}?status=${status}&msg=${msg// /%20}&ping=" || true
}
if check_phoenixd; then
push_to_uptime_kuma "up" "OK"
exit 0
else
push_to_uptime_kuma "down" "phoenixd not responding"
exit 1
fi
owner: root
group: root
mode: "0755"
- name: Create phoenixd health check systemd service
copy:
dest: "/etc/systemd/system/{{ phoenixd_healthcheck_service_name }}.service"
content: |
[Unit]
Description=phoenixd Health Check
After=network.target phoenixd.service
[Service]
Type=oneshot
User=root
ExecStart={{ phoenixd_healthcheck_script_path }}
Environment=UPTIME_KUMA_PUSH_URL=
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
owner: root
group: root
mode: "0644"
- name: Create phoenixd health check systemd timer
copy:
dest: "/etc/systemd/system/{{ phoenixd_healthcheck_service_name }}.timer"
content: |
[Unit]
Description=phoenixd Health Check Timer
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
Persistent=true
[Install]
WantedBy=timers.target
owner: root
group: root
mode: "0644"
- name: Reload systemd daemon after health check units
systemd:
daemon_reload: yes
- name: Enable and start phoenixd health check timer
systemd:
name: "{{ phoenixd_healthcheck_service_name }}.timer"
enabled: yes
state: started
# ===========================================
# Uptime Kuma Push Monitor Setup
# ===========================================
- name: Create Uptime Kuma push monitor setup script for phoenixd
delegate_to: localhost
become: no
copy:
dest: /tmp/setup_phoenixd_monitor.py
content: |
#!/usr/bin/env python3
import sys
import time
import traceback
import yaml
from uptime_kuma_api import UptimeKumaApi, MonitorType
try:
with open('/tmp/ansible_phoenixd_config.yml', 'r') as f:
config = yaml.safe_load(f)
url = config['uptime_kuma_url']
username = config['username']
password = config['password']
monitor_name = config['monitor_name']
api = UptimeKumaApi(url, timeout=30)
api.login(username, password)
monitors = api.get_monitors()
# Find or create "services" group
group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), None)
if not group:
try:
api.add_monitor(type='group', name='services')
except Exception:
time.sleep(2)
monitors = api.get_monitors()
group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), None)
# Get ntfy notification ID
notifications = api.get_notifications()
ntfy_notification_id = None
for notif in notifications:
if notif.get('type') == 'ntfy':
ntfy_notification_id = notif.get('id')
break
existing = next((m for m in monitors if m.get('name') == monitor_name), None)
push_url = None
if existing:
print(f"Monitor '{monitor_name}' already exists (ID: {existing['id']})")
push_token = existing.get('pushToken') or existing.get('push_token')
if push_token:
push_url = f"{url}/api/push/{push_token}"
else:
print(f"Creating push monitor '{monitor_name}'...")
try:
api.add_monitor(
type=MonitorType.PUSH,
name=monitor_name,
parent=group['id'],
interval=90,
maxretries=3,
retryInterval=60,
notificationIDList={ntfy_notification_id: True} if ntfy_notification_id else {}
)
except Exception as e:
# socketio timeout: add_monitor may have succeeded server-side
print(f"add_monitor raised (possibly timeout): {e}", file=sys.stderr)
time.sleep(2)
monitors = api.get_monitors()
new_monitor = next((m for m in monitors if m.get('name') == monitor_name), None)
if new_monitor:
push_token = new_monitor.get('pushToken') or new_monitor.get('push_token')
if push_token:
push_url = f"{url}/api/push/{push_token}"
api.disconnect()
if push_url:
print(f"PUSH_URL={push_url}")
with open('/tmp/phoenixd_push_url.txt', 'w') as f:
f.write(push_url)
print("SUCCESS")
except Exception as e:
print(f"ERROR: {str(e)}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
sys.exit(1)
mode: "0755"
- name: Create temporary config for push monitor setup
delegate_to: localhost
become: no
copy:
dest: /tmp/ansible_phoenixd_config.yml
content: |
uptime_kuma_url: "{{ uptime_kuma_api_url }}"
username: "{{ uptime_kuma_username }}"
password: "{{ uptime_kuma_password }}"
monitor_name: "{{ phoenixd_monitor_name }}"
mode: "0644"
- name: Run Uptime Kuma push monitor setup
command: python3 /tmp/setup_phoenixd_monitor.py
delegate_to: localhost
become: no
register: monitor_setup
changed_when: "'SUCCESS' in monitor_setup.stdout"
ignore_errors: yes
- name: Display monitor setup output
debug:
msg: "{{ monitor_setup.stdout_lines }}"
when: monitor_setup.stdout is defined
- name: Read push URL from file
slurp:
src: /tmp/phoenixd_push_url.txt
delegate_to: localhost
become: no
register: push_url_file
ignore_errors: yes
- name: Parse push URL
set_fact:
phoenixd_push_url: "{{ push_url_file.content | b64decode | trim }}"
when: push_url_file.content is defined
- name: Update health check service with push URL
lineinfile:
path: "/etc/systemd/system/{{ phoenixd_healthcheck_service_name }}.service"
regexp: "^Environment=UPTIME_KUMA_PUSH_URL="
line: "Environment=UPTIME_KUMA_PUSH_URL={{ phoenixd_push_url }}"
when: phoenixd_push_url is defined
notify: Restart phoenixd health check timer
- name: Clean up temporary files
delegate_to: localhost
become: no
file:
path: "{{ item }}"
state: absent
loop:
- /tmp/setup_phoenixd_monitor.py
- /tmp/ansible_phoenixd_config.yml
- /tmp/phoenixd_push_url.txt
# ===========================================
# Post-install Notes
# ===========================================
- name: Display post-install information
debug:
msg: |
✓ phoenixd {{ phoenixd_version }} deployed
Status: systemctl status phoenixd
Logs: journalctl -u phoenixd -f
CLI: sudo PHOENIX_DATADIR={{ phoenixd_data_dir }} phoenix-cli --http-bind-port {{ phoenixd_http_bind_port }} getinfo
HTTP API: http://{{ phoenixd_http_bind_ip }}:{{ phoenixd_http_bind_port }} (loopback only)
Data dir: {{ phoenixd_data_dir }}
API password (needed to wire LNBits up to this node):
sudo grep '^http-password=' {{ phoenixd_data_dir }}/phoenix.conf
⚠️ BACK UP THE SEED NOW: {{ phoenixd_data_dir }}/seed.dat
Losing it means losing the funds. Run
services/phoenixd/setup_backup_phoenixd_to_lapy.yml and also keep
the 12 words somewhere offline.
handlers:
- name: Restart phoenixd
systemd:
name: phoenixd
state: restarted
daemon_reload: yes
- name: Restart phoenixd health check timer
systemd:
name: "{{ phoenixd_healthcheck_service_name }}.timer"
state: restarted
daemon_reload: yes