Initial VPS operations handbook

This commit is contained in:
windyboy
2026-08-03 12:26:42 +08:00
commit b73125e5bc
97 changed files with 3641 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
---
restic_enabled: false
restic_binary: /usr/bin/restic
restic_config_path: /etc/vps-restic/repository.env
restic_state_dir: /var/lib/vps-restic
restic_log_dir: /var/log/vps-restic
restic_backup_on_calendar: '*-*-* 04:30:00'
restic_check_on_calendar: 'Sun *-*-* 05:30:00'
restic_forget_on_calendar: 'Sat *-*-* 05:30:00'
restic_keep_daily: 7
restic_keep_weekly: 4
restic_keep_monthly: 6
restic_sources:
vaultwarden:
- /opt/vaultwarden/backups
- /opt/vaultwarden/vw-data
pdns:
- /opt/pdns/backup
- /opt/pdns/auth/pdns.conf
# Mailcow is deliberately excluded pending its official consistency and restore
# design review. No repository/backend value is supplied by this project.
+4
View File
@@ -0,0 +1,4 @@
---
- name: Reload systemd
ansible.builtin.systemd_service:
daemon_reload: true
+97
View File
@@ -0,0 +1,97 @@
---
- name: Require explicit Restic opt-in
ansible.builtin.assert:
that:
- restic_enabled | bool
fail_msg: >-
Restic is disabled by default. Set restic_enabled=true and provision the
repository configuration only on the target host after backend approval.
- name: Validate supported Restic source profile
ansible.builtin.assert:
that:
- healthcheck_profile in restic_sources
fail_msg: "No approved Restic source profile for {{ healthcheck_profile }}."
- name: Verify Restic binary exists on target
ansible.builtin.stat:
path: "{{ restic_binary }}"
register: restic_binary_stat
- name: Require target-side Restic binary
ansible.builtin.assert:
that: restic_binary_stat.stat.exists
fail_msg: "Install Restic through an approved host maintenance change first."
- name: Verify target-side repository configuration exists
ansible.builtin.stat:
path: "{{ restic_config_path }}"
register: restic_config_stat
- name: Require root-only repository configuration
ansible.builtin.assert:
that:
- restic_config_stat.stat.exists
- restic_config_stat.stat.mode == '0600'
fail_msg: >-
Provision {{ restic_config_path }} directly on the host with mode 0600.
It must contain RESTIC_REPOSITORY, RESTIC_PASSWORD_FILE, and any backend
credentials; do not commit or pass them via Ansible.
- name: Install Restic state and log directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0750"
loop:
- "{{ restic_state_dir }}"
- "{{ restic_log_dir }}"
- name: Install Restic script directory
ansible.builtin.file:
path: /usr/local/lib/vps-restic
state: directory
owner: root
group: root
mode: "0755"
- name: Install Restic scripts
ansible.builtin.template:
src: "{{ item }}.j2"
dest: "/usr/local/lib/vps-restic/{{ item }}"
owner: root
group: root
mode: "0750"
loop:
- backup
- check
- forget-prune
- name: Install Restic systemd units and timers
ansible.builtin.template:
src: "{{ item }}.j2"
dest: "/etc/systemd/system/{{ item }}"
owner: root
group: root
mode: "0644"
loop:
- vps-restic-backup.service
- vps-restic-backup.timer
- vps-restic-check.service
- vps-restic-check.timer
- vps-restic-forget-prune.service
- vps-restic-forget-prune.timer
notify: Reload systemd
- name: Enable Restic timers
ansible.builtin.systemd_service:
name: "{{ item }}"
enabled: true
state: started
daemon_reload: true
loop:
- vps-restic-backup.timer
- vps-restic-check.timer
- vps-restic-forget-prune.timer
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# Repository and password credentials are host-local in {{ restic_config_path }}.
# shellcheck source=/dev/null
source '{{ restic_config_path }}'
exec '{{ restic_binary }}' backup --tag '{{ healthcheck_profile }}' --tag "$(hostname -s)" {% for source in restic_sources[healthcheck_profile] %}{{ source | quote }} {% endfor %}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck source=/dev/null
source '{{ restic_config_path }}'
exec '{{ restic_binary }}' check --read-data-subset=5%
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck source=/dev/null
source '{{ restic_config_path }}'
exec '{{ restic_binary }}' forget --prune --keep-daily {{ restic_keep_daily }} --keep-weekly {{ restic_keep_weekly }} --keep-monthly {{ restic_keep_monthly }} --tag '{{ healthcheck_profile }}'
@@ -0,0 +1,11 @@
[Unit]
Description=Restic backup for approved {{ healthcheck_profile }} sources
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
Group=root
UMask=0077
ExecStart=/usr/local/lib/vps-restic/backup
@@ -0,0 +1,11 @@
[Unit]
Description=Daily Restic backup timer
[Timer]
OnCalendar={{ restic_backup_on_calendar }}
Persistent=true
RandomizedDelaySec=20m
Unit=vps-restic-backup.service
[Install]
WantedBy=timers.target
@@ -0,0 +1,9 @@
[Unit]
Description=Restic repository integrity check
[Service]
Type=oneshot
User=root
Group=root
UMask=0077
ExecStart=/usr/local/lib/vps-restic/check
@@ -0,0 +1,11 @@
[Unit]
Description=Weekly Restic integrity check timer
[Timer]
OnCalendar={{ restic_check_on_calendar }}
Persistent=true
RandomizedDelaySec=30m
Unit=vps-restic-check.service
[Install]
WantedBy=timers.target
@@ -0,0 +1,9 @@
[Unit]
Description=Restic retention and prune
[Service]
Type=oneshot
User=root
Group=root
UMask=0077
ExecStart=/usr/local/lib/vps-restic/forget-prune
@@ -0,0 +1,11 @@
[Unit]
Description=Weekly Restic retention timer
[Timer]
OnCalendar={{ restic_forget_on_calendar }}
Persistent=true
RandomizedDelaySec=30m
Unit=vps-restic-forget-prune.service
[Install]
WantedBy=timers.target