This commit is contained in:
Mike McFetridge
2026-07-20 09:23:17 -04:00
parent c1315882da
commit 72272e4006
3179 changed files with 562960 additions and 14 deletions
@@ -0,0 +1,156 @@
---
- name: Docker postgres backup
hosts: all
become: true
gather_facts: false
vars:
backup_root: "/opt/docker-db-backups/postgres"
backup_timestamp: "{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}"
backup_file_name: "backup_postgres_<< container_name >>_<< backup_timestamp >>.sql.gz"
backup_file_path: "<< backup_root >>/<< backup_file_name >>"
tasks:
- name: Ensure backup root exists
ansible.builtin.file:
path: "<< backup_root >>"
state: directory
mode: "0750"
- name: Get container information
community.docker.docker_container_info:
name: "<< container_name >>"
register: container_info
- name: Fail if container does not exist
ansible.builtin.fail:
msg: "Container '<< container_name >>' does not exist"
when: not container_info.exists
- name: Run backup and validation
block:
- name: Dump all databases with password
ansible.builtin.shell: |
set -euo pipefail
docker exec -e PGPASSWORD=<< postgres_password | quote >> "<< container_name >>" \
pg_dumpall -U "<< postgres_user | default('postgres') >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- postgres_database | default('all') == 'all'
- (postgres_password | default('')) | length > 0
- name: Dump all databases without password
ansible.builtin.shell: |
set -euo pipefail
docker exec "<< container_name >>" \
pg_dumpall -U "<< postgres_user | default('postgres') >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- postgres_database | default('all') == 'all'
- (postgres_password | default('')) | length == 0
- name: Dump selected database with password
ansible.builtin.shell: |
set -euo pipefail
docker exec -e PGPASSWORD=<< postgres_password | quote >> "<< container_name >>" \
pg_dump -U "<< postgres_user | default('postgres') >>" \
"<< postgres_database >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- postgres_database | default('all') != 'all'
- (postgres_password | default('')) | length > 0
- name: Dump selected database without password
ansible.builtin.shell: |
set -euo pipefail
docker exec "<< container_name >>" \
pg_dump -U "<< postgres_user | default('postgres') >>" \
"<< postgres_database >>" \
| gzip > "<< backup_file_path >>"
args:
executable: /bin/bash
changed_when: true
when:
- postgres_database | default('all') != 'all'
- (postgres_password | default('')) | length == 0
- name: Validate backup file exists
ansible.builtin.stat:
path: "<< backup_file_path >>"
register: backup_archive_stat
- name: Fail if backup file is missing or empty
ansible.builtin.fail:
msg: "Backup file missing or empty: << backup_file_path >>"
when:
- not (backup_archive_stat.stat.exists | default(false))
or (backup_archive_stat.stat.size | default(0) | int) == 0
- name: Validate gzip archive integrity
ansible.builtin.command: gzip -t "<< backup_file_path >>"
changed_when: false
- name: Validate postgres dump signature
ansible.builtin.shell: |
set -euo pipefail
gzip -dc "<< backup_file_path >>" \
| head -n 50 \
| grep -Eq 'PostgreSQL database dump'
args:
executable: /bin/bash
changed_when: false
- name: Remove old postgres backup archives
ansible.builtin.find:
paths: "<< backup_root >>"
patterns: "backup_postgres_<< container_name >>_*.sql.gz"
age: "<< backup_retention_days | default(14) >>d"
recurse: true
register: old_backups
- name: Delete old postgres backup archives
ansible.builtin.file:
path: "<< item.path >>"
state: absent
loop: "<< old_backups.files >>"
- name: Mark backup status success
ansible.builtin.set_fact:
backup_status: "success"
backup_status_message: "Backup and validation succeeded"
rescue:
- name: Mark backup status failed
ansible.builtin.set_fact:
backup_status: "failed"
backup_status_message: "<< ansible_failed_result.msg | default('Backup or validation failed') >>"
always:
- name: Send postgres backup status to Discord
ansible.builtin.uri:
url: "<< discord_webhook | default('') >>"
method: POST
body_format: json
body:
content: |
PostgreSQL Backup << backup_status | default('failed') | upper >>
Host: << inventory_hostname >>
Container: << container_name >>
File: << backup_file_path >>
Message: << backup_status_message | default('n/a') >>
status_code: 204
when:
- send_discord_notification | default(false)
- (discord_webhook | default('')) | length > 0
- name: Fail play if backup failed
ansible.builtin.fail:
msg: "<< backup_status_message | default('Backup failed') >>"
when: backup_status | default('failed') == 'failed'
@@ -0,0 +1,2 @@
collections:
- name: community.docker
@@ -0,0 +1,187 @@
{
"slug": "docker-postgres-backup",
"kind": "ansible",
"metadata": {
"name": "Docker Postgres Backup",
"description": "Connects to a named PostgreSQL container and creates compressed SQL dumps with configurable retention. Supports optional Discord notifications on success or failure.",
"tags": [
"docker",
"backup",
"postgres",
"database"
],
"icon": {
"provider": "simple-icons",
"id": "postgresql",
"color": "emerald"
},
"draft": true,
"version": {
"name": "1",
"source_dep_name": "manual/docker-postgres-backup"
}
},
"variables": [
{
"title": "Backup",
"name": "backup",
"items": [
{
"name": "container_name",
"type": "str",
"title": "Container Name",
"required": true
},
{
"name": "postgres_user",
"type": "str",
"title": "Postgres User",
"required": false,
"default": "postgres",
"description": "Database user for pg_dump/pg_dumpall",
"config": {
"placeholder": "postgres"
}
},
{
"name": "postgres_password",
"type": "str",
"title": "Postgres Password",
"required": false,
"default": ""
},
{
"name": "postgres_database",
"type": "str",
"title": "Database Name",
"required": false,
"default": "all",
"description": "Database to dump, or 'all' for all databases",
"config": {
"placeholder": "all"
}
},
{
"name": "backup_retention_days",
"type": "int",
"title": "Retention Days",
"required": false,
"default": 14,
"description": "Delete backup archives older than this many days",
"config": {
"slider": true,
"min": 1,
"max": 365,
"step": 1,
"placeholder": "14",
"unit": "days"
}
}
]
},
{
"title": "Notification",
"name": "notification",
"items": [
{
"name": "send_discord_notification",
"type": "bool",
"title": "Send Discord Notification",
"required": false,
"default": false,
"description": "Send success/failure status to Discord webhook"
},
{
"name": "discord_webhook",
"type": "secret",
"title": "Discord Webhook",
"required": false,
"default": "",
"description": "Discord webhook URL for notifications"
}
]
},
{
"title": "Internal",
"name": "internal",
"items": [
{
"name": "backup_status",
"type": "str",
"title": "Backup Status",
"required": false,
"default": ""
},
{
"name": "inventory_hostname",
"type": "str",
"title": "Inventory Hostname",
"required": false,
"default": ""
},
{
"name": "backup_timestamp",
"type": "str",
"title": "Backup Timestamp",
"required": false,
"default": ""
},
{
"name": "item",
"type": "str",
"title": "Loop Item",
"required": false,
"default": ""
},
{
"name": "backup_file_path",
"type": "str",
"title": "Backup File Path",
"required": false,
"default": ""
},
{
"name": "old_backups",
"type": "str",
"title": "Old Backups",
"required": false,
"default": ""
},
{
"name": "backup_root",
"type": "str",
"title": "Backup Root",
"required": false,
"default": "/backups/postgres",
"config": {
"placeholder": "/backups/postgres"
}
},
{
"name": "backup_file_name",
"type": "str",
"title": "Backup File Name",
"required": false,
"default": ""
},
{
"name": "ansible_failed_result",
"type": "str",
"title": "Failed Result",
"required": false,
"default": ""
},
{
"name": "backup_status_message",
"type": "str",
"title": "Backup Status Message",
"required": false,
"default": "",
"config": {
"textarea": true
}
}
]
}
]
}