Files
ansible/playbooks/backup_docker.yml
T
Harwinder Sekhon 5aaa7020ac Fix playbook bugs from review, add Telegram alerts to health and service checks
Review fixes:
- Availability plays: "ping_result is succeeded" is true for unreachable
  hosts, so offline hosts were added to reachable_hosts and failed the
  next play. Test reachability explicitly (7 playbooks).
- bootstrap_lxc: vim-enhanced and mtr-tiny package names, enable CRB next
  to EPEL, add the root key without replacing authorized_keys, validate
  sshd_config edits and restart sshd only on change.
- backup_docker: the command module does not expand $(...); fix rsync
  filter order and add --exclude=*; restart only the containers that were
  stopped, even when rsync fails; read the storage address from the
  inventory; keep semaphore and semaphore-db running.
- new_lxc: community.general.proxmox no longer exists, use
  community.proxmox; node is mini-pc; valid hostname; password or token
  auth; let the module pick the VMID; longer timeout.
- proxmox-backup: surface the vzdump exit code, let vzdump prune with
  keep-last, drop the rename/prune steps that globbed *.zstd.
- time-region: chrony regex no longer comments out its own server line;
  /etc/localtime as a symlink; reuse facts across plays.
- update_pihole_blocklists: FQCN, become: true, changed_when.
- ansible.cfg gathering = smart; update_system only pings ubuntu/rocky.

Telegram alerts:
- health_check: filesystems above disk_warn_pct (default 80), excluding
  efivarfs, and offline hosts.
- service_check: services not running.
- One message per run from localhost, credentials from Semaphore
  (TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID), no_log on the send task,
  prints the alert text when Telegram is not configured.

Claude-Session: https://claude.ai/code/session_01XEizHawueRXUcxQELRZ8kq
2026-09-06 23:06:18 -04:00

121 lines
4.4 KiB
YAML

---
# Backs up the docker host to the storage container: compose files and
# configs from compose_dir while everything runs, then the Docker volumes
# with the containers stopped so the copy is consistent.
#
# Containers in backup_skip_containers are never stopped. Semaphore runs on
# this host, so it and its database stay up (their volumes are copied live).
# root on the docker host needs an SSH key that is authorized on backup_host.
#
# Extra vars (optional):
# backup_host - inventory name of the destination (default storage)
# backup_root - directory on backup_host (default /mnt/storage/Docker_backups)
# backup_skip_containers - default [semaphore, semaphore-db]
- name: Backup Docker compose files and volumes to storage
hosts: docker
become: true
gather_facts: false
vars:
backup_host: storage
backup_root: /mnt/storage/Docker_backups
backup_addr: "{{ hostvars[backup_host].ansible_host }}"
backup_skip_containers:
- semaphore
- semaphore-db
compose_dir: /home/harry
volumes_dir: /var/lib/docker/volumes
rsync_ssh: ssh -o StrictHostKeyChecking=accept-new
tasks:
- name: Ensure backup destination directories exist
ansible.builtin.file:
path: "{{ backup_root }}/{{ item }}"
state: directory
mode: '0755'
loop: [compose, volumes]
delegate_to: "{{ backup_host }}"
# rsync applies the first matching rule: excludes first, then the rule
# that lets it descend into directories, then the wanted files, then
# everything else is dropped.
- name: Rsync compose files and configs (safe while running)
ansible.builtin.command:
argv:
- rsync
- -az
- --delete
- --prune-empty-dirs
- -e
- "{{ rsync_ssh }}"
- --exclude=node_modules/
- --exclude=.git/
- --exclude=__pycache__/
- --include=*/
- --include=docker-compose.yml
- --include=compose.yml
- --include=.env
- --include=*.conf
- --include=*.yml
- --include=*.yaml
- --include=*.json
- --exclude=*
- "{{ compose_dir }}/"
- "root@{{ backup_addr }}:{{ backup_root }}/compose/"
changed_when: true
- name: List running containers
ansible.builtin.command:
argv: [docker, ps, --format, "{{ '{{' }}.Names{{ '}}' }}"]
register: running_containers
changed_when: false
- name: Work out which containers to stop
ansible.builtin.set_fact:
containers_to_stop: "{{ running_containers.stdout_lines | difference(backup_skip_containers) }}"
- name: Copy the volumes with the containers stopped
block:
- name: Stop containers
ansible.builtin.command:
argv: "{{ ['docker', 'stop'] + containers_to_stop }}"
when: containers_to_stop | length > 0
- name: Rsync Docker volumes to storage
ansible.builtin.command:
argv:
- rsync
- -az
- --delete
- -e
- "{{ rsync_ssh }}"
- "{{ volumes_dir }}/"
- "root@{{ backup_addr }}:{{ backup_root }}/volumes/"
changed_when: true
always:
- name: Start the containers that were running before
ansible.builtin.command:
argv: "{{ ['docker', 'start'] + containers_to_stop }}"
when: containers_to_stop | length > 0
- name: Verify containers are running
ansible.builtin.command:
argv: [docker, ps, --format, "{{ '{{' }}.Names{{ '}}' }} - {{ '{{' }}.Status{{ '}}' }}"]
register: container_status
changed_when: false
- name: Backup summary
ansible.builtin.debug:
msg:
- "Destination: {{ backup_host }} ({{ backup_addr }}):{{ backup_root }}"
- "Stopped for the volume copy: {{ containers_to_stop | join(', ') if containers_to_stop else 'none' }}"
- "Kept running: {{ backup_skip_containers | join(', ') }}"
- "Containers running: {{ container_status.stdout_lines | length }} now, {{ running_containers.stdout_lines | length }} before"
- name: Warn when fewer containers are running than before
ansible.builtin.debug:
msg: "WARNING: {{ item }} did not come back up on {{ inventory_hostname }}"
loop: "{{ running_containers.stdout_lines | difference(container_status.stdout_lines | map('regex_replace', ' - .*$', '')) }}"