--- - name: Create Comprehensive Backups hosts: production gather_facts: yes become: no vars: backup_retention_days: "{{ backup_retention_days | default(7) }}" pre_tasks: - name: Ensure backup directory exists file: path: "{{ backups_path }}" state: directory mode: '0755' become: yes - name: Create timestamp for backup set_fact: backup_timestamp: "{{ ansible_date_time.epoch }}" backup_date: "{{ ansible_date_time.date }}" backup_time: "{{ ansible_date_time.time }}" - name: Create backup directory for this run file: path: "{{ backups_path }}/backup_{{ backup_date }}_{{ backup_time }}" state: directory mode: '0755' register: backup_dir become: yes - name: Set backup directory path set_fact: current_backup_dir: "{{ backup_dir.path }}" tasks: - name: Backup PostgreSQL Database when: backup_postgresql | default(true) | bool block: - name: Check if PostgreSQL stack is running shell: docker compose -f {{ stacks_base_path }}/postgresql/docker-compose.yml ps --format json | jq -r '.[] | select(.Service=="postgres") | .State' | grep -q "running" register: postgres_running changed_when: false failed_when: false - name: Get PostgreSQL container name shell: docker compose -f {{ stacks_base_path }}/postgresql/docker-compose.yml ps --format json | jq -r '.[] | select(.Service=="postgres") | .Name' register: postgres_container changed_when: false when: postgres_running.rc == 0 - name: Read PostgreSQL environment variables shell: | cd {{ stacks_base_path }}/postgresql grep -E "^POSTGRES_(DB|USER|PASSWORD)=" .env 2>/dev/null || echo "" register: postgres_env changed_when: false failed_when: false no_log: true - name: Extract PostgreSQL credentials set_fact: postgres_db: "{{ postgres_env.stdout | regex_search('POSTGRES_DB=([^\\n]+)', '\\1') | first | default('michaelschiemer') }}" postgres_user: "{{ postgres_env.stdout | regex_search('POSTGRES_USER=([^\\n]+)', '\\1') | first | default('postgres') }}" postgres_password: "{{ postgres_env.stdout | regex_search('POSTGRES_PASSWORD=([^\\n]+)', '\\1') | first | default('') }}" when: postgres_running.rc == 0 no_log: true - name: Create PostgreSQL backup shell: | cd {{ stacks_base_path }}/postgresql PGPASSWORD="{{ postgres_password }}" docker compose exec -T postgres pg_dump \ -U {{ postgres_user }} \ -d {{ postgres_db }} \ --clean \ --if-exists \ --create \ --no-owner \ --no-privileges \ | gzip > {{ current_backup_dir }}/postgresql_${postgres_db}_{{ backup_date }}_{{ backup_time }}.sql.gz when: postgres_running.rc == 0 no_log: true - name: Verify PostgreSQL backup stat: path: "{{ current_backup_dir }}/postgresql_{{ postgres_db }}_{{ backup_date }}_{{ backup_time }}.sql.gz" register: postgres_backup_file when: postgres_running.rc == 0 - name: Display PostgreSQL backup status debug: msg: "PostgreSQL backup: {{ 'SUCCESS' if (postgres_running.rc == 0 and postgres_backup_file.stat.exists) else 'SKIPPED (PostgreSQL not running)' }}" - name: Backup Application Data when: backup_application_data | default(true) | bool block: - name: Check if production stack is running shell: docker compose -f {{ stacks_base_path }}/production/docker-compose.base.yml -f {{ stacks_base_path }}/production/docker-compose.production.yml ps --format json | jq -r '.[] | select(.Service=="php") | .State' | grep -q "running" register: app_running changed_when: false failed_when: false - name: Backup application storage directory archive: path: "{{ stacks_base_path }}/production/storage" dest: "{{ current_backup_dir }}/application_storage_{{ backup_date }}_{{ backup_time }}.tar.gz" format: gz when: app_running.rc == 0 ignore_errors: yes - name: Backup application logs archive: path: "{{ stacks_base_path }}/production/storage/logs" dest: "{{ current_backup_dir }}/application_logs_{{ backup_date }}_{{ backup_time }}.tar.gz" format: gz when: app_running.rc == 0 ignore_errors: yes - name: Backup application .env file copy: src: "{{ stacks_base_path }}/production/.env" dest: "{{ current_backup_dir }}/application_env_{{ backup_date }}_{{ backup_time }}.env" remote_src: yes when: app_running.rc == 0 ignore_errors: yes - name: Display application backup status debug: msg: "Application data backup: {{ 'SUCCESS' if app_running.rc == 0 else 'SKIPPED (Application not running)' }}" - name: Backup Gitea Data when: backup_gitea | default(true) | bool block: - name: Check if Gitea stack is running shell: docker compose -f {{ stacks_base_path }}/gitea/docker-compose.yml ps --format json | jq -r '.[] | select(.Service=="gitea") | .State' | grep -q "running" register: gitea_running changed_when: false failed_when: false - name: Get Gitea volume name shell: docker compose -f {{ stacks_base_path }}/gitea/docker-compose.yml config --volumes | head -1 register: gitea_volume changed_when: false when: gitea_running.rc == 0 - name: Backup Gitea volume shell: | docker run --rm \ -v {{ gitea_volume.stdout }}:/source:ro \ -v {{ current_backup_dir }}:/backup \ alpine tar czf /backup/gitea_data_{{ backup_date }}_{{ backup_time }}.tar.gz -C /source . when: gitea_running.rc == 0 and gitea_volume.stdout != "" ignore_errors: yes - name: Display Gitea backup status debug: msg: "Gitea backup: {{ 'SUCCESS' if (gitea_running.rc == 0 and gitea_volume.stdout != '') else 'SKIPPED (Gitea not running)' }}" - name: Backup Docker Registry Images (Optional) when: backup_registry | default(false) | bool block: - name: Check if registry stack is running shell: docker compose -f {{ stacks_base_path }}/registry/docker-compose.yml ps --format json | jq -r '.[] | select(.Service=="registry") | .State' | grep -q "running" register: registry_running changed_when: false failed_when: false - name: List registry images shell: | cd {{ stacks_base_path }}/registry docker compose exec -T registry registry garbage-collect --dry-run /etc/docker/registry/config.yml 2>&1 | grep -E "repository|tag" || echo "No images found" register: registry_images changed_when: false when: registry_running.rc == 0 ignore_errors: yes - name: Save registry image list copy: content: "{{ registry_images.stdout }}" dest: "{{ current_backup_dir }}/registry_images_{{ backup_date }}_{{ backup_time }}.txt" when: registry_running.rc == 0 and registry_images.stdout != "" ignore_errors: yes - name: Display registry backup status debug: msg: "Registry backup: {{ 'SUCCESS' if registry_running.rc == 0 else 'SKIPPED (Registry not running)' }}" - name: Create backup metadata copy: content: | Backup Date: {{ backup_date }} {{ backup_time }} Backup Timestamp: {{ backup_timestamp }} Host: {{ inventory_hostname }} Components Backed Up: - PostgreSQL: {{ 'YES' if ((backup_postgresql | default(true) | bool) and (postgres_running.rc | default(1) == 0)) else 'NO' }} - Application Data: {{ 'YES' if ((backup_application_data | default(true) | bool) and (app_running.rc | default(1) == 0)) else 'NO' }} - Gitea: {{ 'YES' if ((backup_gitea | default(true) | bool) and (gitea_running.rc | default(1) == 0)) else 'NO' }} - Registry: {{ 'YES' if ((backup_registry | default(false) | bool) and (registry_running.rc | default(1) == 0)) else 'NO' }} Backup Location: {{ current_backup_dir }} dest: "{{ current_backup_dir }}/backup_metadata.txt" mode: '0644' - name: Verify backup files when: verify_backups | default(true) | bool block: - name: List all backup files find: paths: "{{ current_backup_dir }}" file_type: file register: backup_files - name: Check backup file sizes stat: path: "{{ item.path }}" register: backup_file_stats loop: "{{ backup_files.files }}" - name: Display backup summary debug: msg: | Backup Summary: - Total files: {{ backup_files.files | length }} - Total size: {{ backup_file_stats.results | map(attribute='stat.size') | sum | int / 1024 / 1024 }} MB - Location: {{ current_backup_dir }} - name: Fail if no backup files created fail: msg: "No backup files were created in {{ current_backup_dir }}" when: backup_files.files | length == 0 - name: Cleanup old backups block: - name: Find old backup directories find: paths: "{{ backups_path }}" patterns: "backup_*" file_type: directory register: backup_dirs - name: Calculate cutoff date set_fact: cutoff_timestamp: "{{ (ansible_date_time.epoch | int) - (backup_retention_days | int * 86400) }}" - name: Remove old backup directories file: path: "{{ item.path }}" state: absent loop: "{{ backup_dirs.files }}" when: item.mtime | int < cutoff_timestamp | int become: yes - name: Display cleanup summary debug: msg: "Cleaned up backups older than {{ backup_retention_days }} days" post_tasks: - name: Display final backup status debug: msg: | ========================================== Backup completed successfully! ========================================== Backup location: {{ current_backup_dir }} Retention: {{ backup_retention_days }} days ==========================================