feat(rustdesk): onboard self-hosted RustDesk server on hk2
Deploy hbbs + hbbr via a safe-by-default Ansible role (playbooks/rustdesk.yml): with rustdesk_confirm=false it only reports whether compose.yml matches live state and refuses to recreate the stack; with rustdesk_confirm=true it deploys and recreates. The relay assert rejects the known-bad hk2.wsvc.info hostname. Add health profiles rustdesk (hbbs/hbbr health, relay DNS) and hk2aux (co-located traefik/adguard/remark42 on hk2), plus the rustdesk-health runbook and AGENTS.md entry. Server image pinned rustdesk/rustdesk-server:1.1.14.
This commit is contained in:
@@ -58,6 +58,8 @@ Also readable as `agent.md` (symlink → this file).
|
||||
|
||||
**Matrix (ESS on synapse):** Matrix homeserver running on `synapse.chans.xyz` via the official ESS (Element Server Suite) Helm chart with Synapse + MAS + Element Web + Admin. DNS zone `chans.xyz` managed by hk2 PowerDNS. Before changing config, read [docs/matrix-upstream.md](docs/matrix-upstream.md) and [hosts/synapse.chans.xyz.md](hosts/synapse.chans.xyz.md). K3s cluster on this node has hostPort 80/443 for Traefik (no ServiceLB). Health: [matrix-health](runbooks/matrix-health.md).
|
||||
|
||||
**RustDesk:** Self-hosted RustDesk server on `hk2.chans.xyz` (`/opt/rustdesk`, containers `hbbs`/`hbbr`, image pinned `1.1.14`). The `hbbs -r` relay hostname must resolve to the host's public IP `154.36.174.161` — use `hk2.chans.xyz` (never `hk2.wsvc.info`, which has no DNS record). Health: [rustdesk-health](runbooks/rustdesk-health.md).
|
||||
|
||||
## Runbooks & scripts
|
||||
|
||||
| Task | Path |
|
||||
@@ -71,6 +73,7 @@ Also readable as `agent.md` (symlink → this file).
|
||||
| PowerDNS health (hk2) | [runbooks/pdns-health.md](runbooks/pdns-health.md) |
|
||||
| PowerDNS upstream refs | [docs/pdns-upstream.md](docs/pdns-upstream.md) |
|
||||
| Matrix health | [runbooks/matrix-health.md](runbooks/matrix-health.md) |
|
||||
| RustDesk health (hk2) | [runbooks/rustdesk-health.md](runbooks/rustdesk-health.md) |
|
||||
| AdGuard Home health | [runbooks/adguard-home-health.md](runbooks/adguard-home-health.md) |
|
||||
| Matrix upstream refs | [docs/matrix-upstream.md](docs/matrix-upstream.md) |
|
||||
| Hermes Agent Matrix channel | [docs/hermes-matrix.md](docs/hermes-matrix.md) |
|
||||
@@ -90,6 +93,10 @@ Issues are tracked in Linear and created/updated via the Linear MCP (`vps` proje
|
||||
|
||||
Default triage labels: needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix. See `docs/agents/triage-labels.md`.
|
||||
|
||||
### Domain docs
|
||||
|
||||
Single-context layout: `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.
|
||||
|
||||
## Safety
|
||||
|
||||
- Never commit secrets: passwords, API keys, private keys, `.env`, `mailcow.conf` DB passwords, Vaultwarden `ADMIN_TOKEN` / `.smtp-credentials`.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
# Deploy/reconcile the self-hosted RustDesk server (hbbs + hbbr) on hk2.
|
||||
#
|
||||
# Safe by default: run with --check for a read-only report, or supply
|
||||
# rustdesk_confirm=true to deploy the compose file and recreate the stack.
|
||||
#
|
||||
# # Read-only report
|
||||
# ansible-playbook playbooks/rustdesk.yml --limit rustdesk --check
|
||||
#
|
||||
# # Apply (deploy compose + recreate hbbs/hbbr)
|
||||
# ansible-playbook playbooks/rustdesk.yml --limit rustdesk \
|
||||
# -e '{"rustdesk_confirm": true}'
|
||||
- name: Deploy and reconcile RustDesk server
|
||||
hosts: rustdesk
|
||||
become: true
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
roles:
|
||||
- role: rustdesk
|
||||
tags: [rustdesk, mutating]
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
source '{{ healthcheck_install_root }}/health-common.sh'
|
||||
|
||||
require_command docker
|
||||
require_command ss
|
||||
|
||||
# Auxiliary services co-located on hk2.chans.xyz (separate compose projects
|
||||
# under /opt, fronted by Traefik). Verified live 2026-08-12.
|
||||
|
||||
# traefik
|
||||
if docker inspect traefik >/dev/null 2>&1; then
|
||||
[[ "$(docker inspect traefik --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)" == true ]] \
|
||||
&& record ok 'traefik_running' || record critical 'traefik_not_running'
|
||||
else
|
||||
record critical 'traefik_container_missing'
|
||||
fi
|
||||
|
||||
# adguardhome (hk2 variant: DoH 5443, DoT 853)
|
||||
if docker inspect adguardhome >/dev/null 2>&1; then
|
||||
[[ "$(docker inspect adguardhome --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)" == true ]] \
|
||||
&& record ok 'adguard_running' || record critical 'adguard_not_running'
|
||||
else
|
||||
record critical 'adguard_container_missing'
|
||||
fi
|
||||
|
||||
# remark42
|
||||
if docker inspect remark42 >/dev/null 2>&1; then
|
||||
[[ "$(docker inspect remark42 --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)" == true ]] \
|
||||
&& record ok 'remark42_running' || record critical 'remark42_not_running'
|
||||
else
|
||||
record critical 'remark42_container_missing'
|
||||
fi
|
||||
|
||||
# nginx-manager was removed 2026-08-12 (leftover config, never running).
|
||||
# Warn if a container by that name ever reappears.
|
||||
if docker inspect nginx-manager >/dev/null 2>&1; then
|
||||
record warning 'nginx_manager_unexpectedly_running'
|
||||
else
|
||||
record ok 'nginx_manager_not_running'
|
||||
fi
|
||||
|
||||
# Listening ports (Traefik 80/443/8080, AdGuard DoH 5443 / DoT 853).
|
||||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)80$' \
|
||||
&& record ok 'traefik_http_80' || record critical 'traefik_http_80_missing'
|
||||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)443$' \
|
||||
&& record ok 'traefik_https_443' || record critical 'traefik_https_443_missing'
|
||||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)8080$' \
|
||||
&& record ok 'traefik_dashboard_8080' || record warning 'traefik_dashboard_8080_missing'
|
||||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)5443$' \
|
||||
&& record ok 'adguard_doh_5443' || record critical 'adguard_doh_5443_missing'
|
||||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)853$' \
|
||||
&& record ok 'adguard_dot_853' || record critical 'adguard_dot_853_missing'
|
||||
|
||||
emit_result
|
||||
exit "$EXIT_CODE"
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
source '{{ healthcheck_install_root }}/health-common.sh'
|
||||
|
||||
require_command docker
|
||||
require_command dig
|
||||
|
||||
# hbbs / hbbr must both be running (separate compose project at /opt/rustdesk).
|
||||
output="$(docker compose --project-directory /opt/rustdesk ps --all 2>&1)"
|
||||
if grep -qiE 'Exited|Restarting|[[:space:]]Dead[[:space:]]' <<<"$output"; then
|
||||
record critical 'rustdesk_unhealthy_container'
|
||||
else
|
||||
record ok 'rustdesk_compose_ok'
|
||||
fi
|
||||
|
||||
# hbbs must advertise the relay hostname that resolves to this host's public IP.
|
||||
cmd="$(docker inspect hbbs --format '{{ '{{' }}json .Config.Cmd{{ '}}' }}' 2>/dev/null)" || record critical 'rustdesk_hbbs_missing'
|
||||
grep -q 'hk2.chans.xyz:21117' <<<"$cmd" || record critical 'rustdesk_relay_misconfigured'
|
||||
|
||||
# The advertised relay hostname must resolve to this host's public IP.
|
||||
resolved="$(dig +short hk2.chans.xyz A 2>/dev/null)"
|
||||
grep -q '154.36.174.161' <<<"$resolved" || record critical 'rustdesk_relay_dns_missing'
|
||||
|
||||
emit_result
|
||||
exit "$EXIT_CODE"
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
# RustDesk server deployment (hbbs + hbbr) on hk2.
|
||||
# Safe by default: without rustdesk_confirm=true the role only reports whether
|
||||
# the declared compose file matches live state and refuses to recreate the stack.
|
||||
rustdesk_confirm: false
|
||||
# Compose project directory.
|
||||
rustdesk_compose_dir: /opt/rustdesk
|
||||
# Relay (hbbr) hostname:port advertised to every client via `hbbs -r`.
|
||||
# MUST resolve to this host's public IP (154.36.174.161). The known-bad value
|
||||
# 'hk2.wsvc.info' has no DNS record and must never be used.
|
||||
rustdesk_relay: hk2.chans.xyz:21117
|
||||
# Pinned server image (used for both hbbs and hbbr).
|
||||
rustdesk_image: rustdesk/rustdesk-server:1.1.14
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
# Deploy/reconcile the self-hosted RustDesk server (hbbs + hbbr).
|
||||
# Idempotent: deploys the declared compose file; only recreates the stack with
|
||||
# explicit confirmation.
|
||||
|
||||
- name: Validate relay address is set and not the known-bad value
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- rustdesk_relay | length > 0
|
||||
- "'hk2.wsvc.info' not in rustdesk_relay"
|
||||
fail_msg: >-
|
||||
rustdesk_relay must be a resolvable relay address. The known-bad
|
||||
'hk2.wsvc.info' has no DNS record and must not be used.
|
||||
|
||||
- name: Ensure compose project directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ rustdesk_compose_dir }}"
|
||||
state: directory
|
||||
owner: windy
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Deploy compose file
|
||||
ansible.builtin.template:
|
||||
src: compose.yml.j2
|
||||
dest: "{{ rustdesk_compose_dir }}/compose.yml"
|
||||
owner: windy
|
||||
group: windy
|
||||
mode: "0644"
|
||||
register: rustdesk_compose_deployed
|
||||
|
||||
- name: Report no change needed
|
||||
ansible.builtin.debug:
|
||||
msg: "compose.yml already matches declared state; no change needed."
|
||||
when: not rustdesk_compose_deployed.changed
|
||||
|
||||
- name: Refuse to recreate without explicit confirmation
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
compose.yml differs from declared state but rustdesk_confirm is not true.
|
||||
Supply rustdesk_confirm=true to deploy the file and recreate the stack.
|
||||
when:
|
||||
- rustdesk_compose_deployed.changed
|
||||
- not (rustdesk_confirm | bool)
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Apply compose stack
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- compose
|
||||
- --project-directory
|
||||
- "{{ rustdesk_compose_dir }}"
|
||||
- up
|
||||
- -d
|
||||
when:
|
||||
- rustdesk_compose_deployed.changed
|
||||
- rustdesk_confirm | bool
|
||||
changed_when: true
|
||||
register: rustdesk_apply
|
||||
|
||||
- name: Verify hbbs relay command
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- inspect
|
||||
- hbbs
|
||||
- --format
|
||||
- '{{ "{{" }}json .Config.Cmd{{ "}}" }}'
|
||||
register: rustdesk_hbbs_cmd
|
||||
changed_when: false
|
||||
when:
|
||||
- rustdesk_compose_deployed.changed
|
||||
- rustdesk_confirm | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Assert hbbs advertises the declared relay
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'{{ rustdesk_relay }}' in rustdesk_hbbs_cmd.stdout"
|
||||
fail_msg: "hbbs is not advertising the declared relay {{ rustdesk_relay }}."
|
||||
when:
|
||||
- rustdesk_compose_deployed.changed
|
||||
- rustdesk_confirm | bool
|
||||
- not ansible_check_mode
|
||||
@@ -0,0 +1,34 @@
|
||||
networks:
|
||||
rustdesk-net:
|
||||
external: false
|
||||
|
||||
services:
|
||||
hbbs:
|
||||
container_name: hbbs
|
||||
ports:
|
||||
- 21115:21115
|
||||
- 21116:21116
|
||||
- 21116:21116/udp
|
||||
- 21118:21118
|
||||
image: {{ rustdesk_image }}
|
||||
command: "hbbs -r {{ rustdesk_relay }}"
|
||||
volumes:
|
||||
- ./hbbs:/root
|
||||
networks:
|
||||
- rustdesk-net
|
||||
depends_on:
|
||||
- hbbr
|
||||
restart: unless-stopped
|
||||
|
||||
hbbr:
|
||||
container_name: hbbr
|
||||
ports:
|
||||
- 21117:21117
|
||||
- 21119:21119
|
||||
image: {{ rustdesk_image }}
|
||||
command: hbbr
|
||||
volumes:
|
||||
- ./hbbr:/root
|
||||
networks:
|
||||
- rustdesk-net
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,80 @@
|
||||
# RustDesk server health (hk2)
|
||||
|
||||
Read-only checks for the `/opt/rustdesk` stack on **hk2.chans.xyz**.
|
||||
|
||||
Facts: [hosts/hk2.chans.xyz.md](../hosts/hk2.chans.xyz.md)
|
||||
|
||||
Prefer the Ansible health report for routine checks and the
|
||||
[rustdesk playbook](../ansible/playbooks/rustdesk.yml) for reviewed config
|
||||
changes. Keep the SSH commands below for focused diagnosis.
|
||||
|
||||
## Focused SSH diagnostics
|
||||
|
||||
Use these only after the Ansible health report needs investigation.
|
||||
|
||||
### Quick remote
|
||||
|
||||
```bash
|
||||
ssh -4 windy@hk2.chans.xyz 'cd /opt/rustdesk && docker compose ps -a'
|
||||
```
|
||||
|
||||
Expect `hbbs` and `hbbr` both `Up`. Ignore any stopped orphans unless cleaning up.
|
||||
|
||||
### Version / image pin
|
||||
|
||||
Expect `rustdesk/rustdesk-server:1.1.14` (or the documented pin). Startup logs
|
||||
should show the relay list and no errors.
|
||||
|
||||
```bash
|
||||
ssh -4 windy@hk2.chans.xyz 'docker inspect hbbs --format "{{.Config.Image}}"; docker inspect hbbr --format "{{.Config.Image}}"'
|
||||
```
|
||||
|
||||
### Relay address (the critical check)
|
||||
|
||||
The `hbbs -r <host>:21117` value is advertised to every client as the relay
|
||||
(hbbr) address. It **must resolve to this host's public IP** `154.36.174.161`.
|
||||
`hk2.chans.xyz` is the correct value; `hk2.wsvc.info` has no DNS record and must
|
||||
never be used.
|
||||
|
||||
```bash
|
||||
# The value hbbs is actually serving (definitive):
|
||||
ssh -4 windy@hk2.chans.xyz 'docker inspect hbbs --format "{{json .Config.Cmd}}"'
|
||||
# expect ["hbbs","-r","hk2.chans.xyz:21117"]
|
||||
|
||||
# Confirm the relay hostname resolves to the public IP:
|
||||
dig +short hk2.chans.xyz A @1.1.1.1 # expect 154.36.174.161
|
||||
|
||||
# Confirm the relay hostname is what hbbs logs as its relay list:
|
||||
ssh -4 windy@hk2.chans.xyz 'docker logs hbbs 2>&1 | grep "relay-servers" | tail -1'
|
||||
# expect relay-servers=["hk2.chans.xyz:21117"]
|
||||
```
|
||||
|
||||
### Ports (from laptop / WSL)
|
||||
|
||||
```bash
|
||||
IP=154.36.174.161
|
||||
nc -vz -w 5 $IP 21117 # hbbr relay TCP
|
||||
nc -vz -w 5 $IP 21116 # hbbs rendezvous TCP
|
||||
nc -vz -u -w 5 $IP 21116 # hbbs rendezvous UDP
|
||||
nc -vz -w 5 $IP 21115 # hbbs NAT type test
|
||||
```
|
||||
|
||||
## Scheduled local check
|
||||
|
||||
The sanitized Ansible health profile is `rustdesk` (deployed via
|
||||
`ansible/playbooks/healthchecks.yml` on hosts whose inventory lists it). It runs
|
||||
locally through `vps-healthcheck.timer`, writes a sanitized JSON result to
|
||||
`/var/lib/vps-health/latest.json`, and never exposes secrets.
|
||||
|
||||
## After config changes
|
||||
|
||||
Reviewed changes to `/opt/rustdesk/compose.yml` (relay address, image pin, port
|
||||
mapping) are applied through the gated rustdesk playbook:
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-playbook playbooks/rustdesk.yml --limit rustdesk \
|
||||
-e '{"rustdesk_confirm": true}'
|
||||
```
|
||||
|
||||
Re-run the health report afterwards.
|
||||
Reference in New Issue
Block a user