D7.5 Backup & Restore Volumes με Ansible 🔄
Η στρατηγική backup
Τα Docker volumes περιέχουν τα πιο κρίσιμα δεδομένα της υποδομής σου (databases, uploads, config). Η Ansible δίνει ένα ισχυρό, idempotent pattern για backup και restore.
Backup Strategy:
│
├── Consistency:
│ └── Σταμάτα container → backup → ξεκίνα ξανά
│ (ή: flush tables σε MySQL/PostgreSQL)
│
├── Storage:
│ ├── Τοπικό (στον ίδιο host)
│ ├── Remote (rsync σε backup server)
│ └── Object storage (S3, MinIO)
│
└── Retention:
└── Κράτα Ν ημέρες, διέγραψε παλιά
Pattern 1: Tar backup με helper container
Ο πιο portable τρόπος — χρησιμοποιεί έναν temporary container:
tasks:
# ── Δημιουργία backup directory ────────────
- name: Backup directory
ansible.builtin.file:
path: "/opt/backups/docker/{{ inventory_hostname }}"
state: directory
mode: '0750'
# ── Backup named volume με temp container ──
- name: "Backup volume: {{ volume_name }}"
community.docker.docker_container:
name: backup_tmp
image: alpine:latest
state: started
auto_remove: true # ← αυτόματη διαγραφή
detach: false # ← περιμένει να τελειώσει
command: >
tar czf /backup/{{ volume_name }}-{{ ansible_date_time.date }}.tar.gz
-C /data .
volumes:
- "{{ volume_name }}:/data:ro" # ← το volume
- "/opt/backups/docker/{{ inventory_hostname }}:/backup:rw"
# ↑ ro: δεν αλλάζουμε τίποτα στο volume!
- name: Επαλήθευση backup
ansible.builtin.stat:
path: "/opt/backups/docker/{{ inventory_hostname }}/{{ volume_name }}-{{ ansible_date_time.date }}.tar.gz"
register: backup_file
- name: Αποτέλεσμα backup
ansible.builtin.debug:
msg: >
{{ volume_name }}:
{{ '✅ ' + (backup_file.stat.size | human_readable) if backup_file.stat.exists
else '❌ Backup απέτυχε!' }}
Pattern 2: Database-aware backup (PostgreSQL)
Για databases, χρειάζεσαι consistent backup — όχι απλό tar:
tasks:
# ── PostgreSQL dump ──────────────────────
- name: PostgreSQL backup
community.docker.docker_container:
name: pg_backup_tmp
image: postgres:15-alpine
state: started
auto_remove: true
detach: false
command: >
pg_dumpall
-h postgres_db
-U {{ postgres_user }}
-f /backup/postgres-{{ ansible_date_time.date }}.sql
volumes:
- "/opt/backups/docker/{{ inventory_hostname }}:/backup:rw"
networks:
- name: backend_net # ← ίδιο network με τον postgres!
env:
PGPASSWORD: "{{ vault_postgres_pass }}"
no_log: true
# ── Συμπίεση dump ─────────────────────────
- name: Συμπίεση SQL dump
ansible.builtin.command:
cmd: >
gzip -f
/opt/backups/docker/{{ inventory_hostname }}/postgres-{{ ansible_date_time.date }}.sql
changed_when: true
# ── MySQL dump ────────────────────────────
- name: MySQL backup
community.docker.docker_container:
name: mysql_backup_tmp
image: mysql:8.0
state: started
auto_remove: true
detach: false
command: >
mysqldump
-h mysql_db
-u {{ mysql_user }}
--all-databases
--single-transaction
--quick
--lock-tables=false
volumes:
- "/opt/backups/docker/{{ inventory_hostname }}:/backup:rw"
networks:
- name: backend_net
env:
MYSQL_PWD: "{{ vault_mysql_pass }}"
no_log: true
register: mysql_dump
- name: Αποθήκευση MySQL dump
ansible.builtin.copy:
content: "{{ mysql_dump.Output }}"
dest: "/opt/backups/docker/{{ inventory_hostname }}/mysql-{{ ansible_date_time.date }}.sql"
mode: '0640'
when: not mysql_dump.failed
Pattern 3: Restore από backup
tasks:
# ── Restore named volume ─────────────────
- name: "⚠️ Restore volume: {{ volume_name }}"
block:
# ── Σταμάτα containers ──────────────
- name: Σταμάτα dependent containers
community.docker.docker_container:
name: "{{ item }}"
state: stopped
loop: "{{ containers_using_volume }}"
ignore_errors: true
# ── Restore ────────────────────────
- name: Restore volume από backup
community.docker.docker_container:
name: restore_tmp
image: alpine:latest
state: started
auto_remove: true
detach: false
command: >
sh -c "rm -rf /data/* &&
tar xzf /backup/{{ backup_file_name }}
-C /data ."
volumes:
- "{{ volume_name }}:/data:rw" # ← εγγραφή!
- "/opt/backups/docker/{{ inventory_hostname }}:/backup:ro"
# ── Ξεκίνα containers ─────────────
- name: Ξεκίνα containers
community.docker.docker_container:
name: "{{ item }}"
state: started
loop: "{{ containers_using_volume }}"
rescue:
- name: "❌ Restore απέτυχε!"
ansible.builtin.debug:
msg: "Ελέγξτε το backup αρχείο: {{ backup_file_name }}"
# ── PostgreSQL restore ────────────────────
- name: PostgreSQL restore
community.docker.docker_container:
name: pg_restore_tmp
image: postgres:15-alpine
state: started
auto_remove: true
detach: false
command: >
psql
-h postgres_db
-U {{ postgres_user }}
-f /backup/{{ backup_file_name }}
volumes:
- "/opt/backups/docker/{{ inventory_hostname }}:/backup:ro"
networks:
- name: backend_net
env:
PGPASSWORD: "{{ vault_postgres_pass }}"
no_log: true
Retention — Διατήρηση για N ημέρες
tasks:
- name: Αφαίρεση παλιών backups (> {{ retention_days }} ημέρες)
ansible.builtin.find:
paths: "/opt/backups/docker/{{ inventory_hostname }}"
patterns: "*.tar.gz,*.sql.gz"
age: "{{ retention_days }}d"
age_stamp: mtime
register: old_backups
- name: Διαγραφή παλιών backups
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups.files }}"
loop_control:
label: "{{ item.path | basename }}"
when: old_backups.files | length > 0
- name: Αναφορά retention
ansible.builtin.debug:
msg: >
Διαγράφηκαν {{ old_backups.files | length }}
αρχεία παλιότερα από {{ retention_days }} ημέρες.
Πλήρες Backup Playbook
---
# docker-volume-backup.yml
- name: Docker Volume Backup
hosts: "{{ target | default('all_managed') }}"
become: true
gather_facts: true
vars:
backup_base: /opt/backups/docker
retention_days: 7
backup_date: "{{ ansible_date_time.date }}"
volumes_to_backup:
- name: postgres_data
type: postgres
container: postgres_db
network: backend_net
db_user: "{{ postgres_user }}"
db_pass: "{{ vault_postgres_pass }}"
- name: app_uploads
type: generic
container: myapp
- name: redis_data
type: generic
container: redis
tasks:
# ── Προετοιμασία ──────────────────────────
- name: Backup directory
ansible.builtin.file:
path: "{{ backup_base }}/{{ inventory_hostname }}"
state: directory
mode: '0750'
# ── Backup loop ───────────────────────────
- name: "💾 Backup: {{ item.name }}"
block:
# Generic tar backup
- name: "Tar backup: {{ item.name }}"
community.docker.docker_container:
name: "backup_{{ item.name }}_tmp"
image: alpine:latest
state: started
auto_remove: true
detach: false
command: >
tar czf /backup/{{ item.name }}-{{ backup_date }}.tar.gz
-C /data .
volumes:
- "{{ item.name }}:/data:ro"
- "{{ backup_base }}/{{ inventory_hostname }}:/backup:rw"
when: item.type == 'generic'
# PostgreSQL dump
- name: "PostgreSQL dump: {{ item.name }}"
community.docker.docker_container:
name: "pgdump_{{ item.name }}_tmp"
image: postgres:15-alpine
state: started
auto_remove: true
detach: false
command: >
bash -c "pg_dumpall -h {{ item.container }} -U {{ item.db_user }}
| gzip > /backup/{{ item.name }}-{{ backup_date }}.sql.gz"
volumes:
- "{{ backup_base }}/{{ inventory_hostname }}:/backup:rw"
networks:
- name: "{{ item.network }}"
env:
PGPASSWORD: "{{ item.db_pass }}"
no_log: true
when: item.type == 'postgres'
rescue:
- name: "❌ Backup απέτυχε: {{ item.name }}"
ansible.builtin.debug:
msg: "Αποτυχία backup για {{ item.name }} — συνεχίζω..."
loop: "{{ volumes_to_backup }}"
loop_control:
label: "{{ item.name }}"
# ── Retention cleanup ─────────────────────
- name: "🧹 Αφαίρεση backups > {{ retention_days }} ημέρες"
ansible.builtin.find:
paths: "{{ backup_base }}/{{ inventory_hostname }}"
patterns: "*.tar.gz,*.sql.gz"
age: "{{ retention_days }}d"
age_stamp: mtime
register: old_backups
- name: Διαγραφή παλιών
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups.files }}"
loop_control:
label: "{{ item.path | basename }}"
# ── Report ────────────────────────────────
- name: "📊 Backup Report"
ansible.builtin.find:
paths: "{{ backup_base }}/{{ inventory_hostname }}"
patterns: "*.tar.gz,*.sql.gz"
age: "1d"
age_stamp: mtime
register: today_backups
- name: Εμφάνιση αποτελεσμάτων
ansible.builtin.debug:
msg:
- "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- " Backup Report — {{ inventory_hostname }}"
- " Ημερομηνία: {{ backup_date }}"
- "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- "{% for f in today_backups.files %}\
{{ f.path | basename }}: {{ f.size | human_readable }}\
{% endfor %}"
- " Παλιά (διαγράφηκαν): {{ old_backups.files | length }}"
- "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Εκτέλεση
ansible-playbook playbooks/docker-volume-backup.yml \
--limit nextcloud -v
# Dry run
ansible-playbook playbooks/docker-volume-backup.yml \
--limit nextcloud --check
# Override retention
ansible-playbook playbooks/docker-volume-backup.yml \
--limit nextcloud \
-e "retention_days=14"
Σύνοψη D7.5
Backup & Restore Volumes
│
├── Backup patterns:
│ ├── Generic (tar): alpine container + tar czf
│ └── Database-aware:
│ ├── PostgreSQL: pg_dumpall
│ └── MySQL: mysqldump --single-transaction
│
├── Κλειδί: auto_remove + detach: false
│ ├── auto_remove: true → cleanup αυτόματα
│ └── detach: false → Ansible περιμένει αποτέλεσμα
│
├── Retention:
│ ├── find module → αρχεία > N ημέρες
│ └── file: state=absent → διαγραφή
│
└── Best practices:
✅ Backup σε :ro mount (ασφάλεια)
✅ Consistent DB backups (dump, όχι tar)
✅ Αυτόματο retention cleanup
✅ Verification μετά backup
❌ ΠΟΤΕ docker prune volumes χωρίς έλεγχο