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:
windyboy
2026-08-12 21:16:30 +08:00
parent e7296e664a
commit 035587e3bf
8 changed files with 320 additions and 0 deletions
+20
View File
@@ -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"
+13
View File
@@ -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
+85
View File
@@ -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