#!/bin/sh set -eu # PostgreSQL backup triggered through the Docker Engine unix socket. # Example: ./postgres-dump-zstd-docker-sock.sh --socket=/var/run/docker.sock --api-version=v1.51 --container=postgres17 --backup-dir=/backups --backup-prefix=pgvector17_all_databases_zstd_ # Remote example: curl -fsSL https://git.1-h.cc/Scripts/Linux/raw/branch/main/postgres-dump-zstd-docker-sock.sh | sh -s -- --socket=/var/run/docker.sock --api-version=v1.51 --container=postgres17 --backup-dir=/backups --backup-prefix=pgvector17_all_databases_zstd_ log() { printf '%s\n' "[cron] $(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" } print_usage() { cat <&2 Usage: $0 --socket=PATH --api-version=VERSION --container=NAME --backup-dir=DIR --backup-prefix=PREFIX Options: --socket=PATH Docker Engine unix socket path --api-version=VERSION Docker API version (e.g. v1.51) --container=NAME PostgreSQL container name --backup-dir=DIR Directory to store backups --backup-prefix=PREFIX Backup filename prefix --help Show this help message EOF } DOCKER_SOCKET="" DOCKER_API_VERSION="" PG_CONTAINER_NAME="" BACKUP_DIR="" BACKUP_PREFIX="" missing_value() { printf 'Missing value for %s\n' "$1" >&2 print_usage exit 1 } while [ "$#" -gt 0 ]; do case $1 in --socket=*) DOCKER_SOCKET="${1#*=}" ;; --socket) if [ "$#" -lt 2 ]; then missing_value '--socket' fi shift DOCKER_SOCKET="$1" ;; --api-version=*) DOCKER_API_VERSION="${1#*=}" ;; --api-version) if [ "$#" -lt 2 ]; then missing_value '--api-version' fi shift DOCKER_API_VERSION="$1" ;; --container=*) PG_CONTAINER_NAME="${1#*=}" ;; --container) if [ "$#" -lt 2 ]; then missing_value '--container' fi shift PG_CONTAINER_NAME="$1" ;; --backup-dir=*) BACKUP_DIR="${1#*=}" ;; --backup-dir) if [ "$#" -lt 2 ]; then missing_value '--backup-dir' fi shift BACKUP_DIR="$1" ;; --backup-prefix=*) BACKUP_PREFIX="${1#*=}" ;; --backup-prefix) if [ "$#" -lt 2 ]; then missing_value '--backup-prefix' fi shift BACKUP_PREFIX="$1" ;; --help) print_usage exit 0 ;; --) shift break ;; *) printf 'Unknown option: %s\n' "$1" >&2 print_usage exit 1 ;; esac shift done if [ "$#" -gt 0 ]; then printf 'Unexpected positional arguments: %s\n' "$*" >&2 print_usage exit 1 fi if [ -z "$DOCKER_SOCKET" ] || [ -z "$DOCKER_API_VERSION" ] || [ -z "$PG_CONTAINER_NAME" ] || [ -z "$BACKUP_DIR" ] || [ -z "$BACKUP_PREFIX" ]; then print_usage exit 1 fi if [ ! -S "$DOCKER_SOCKET" ]; then log "docker socket $DOCKER_SOCKET not found" exit 1 fi timestamp=$(date +%Y-%m-%d_%H-%M-%S) backup_path="$BACKUP_DIR/${BACKUP_PREFIX}${timestamp}.sql.zst" docker_api_base="http://localhost/${DOCKER_API_VERSION}" exec_create_endpoint="${docker_api_base}/containers/${PG_CONTAINER_NAME}/exec" log "preparing database backup at $backup_path via docker unix socket" cmd="set -o pipefail && pg_dumpall --username=\"\${POSTGRES_USER:-postgres}\" --clean | zstd > ${backup_path}" create_payload=$(jq -n --arg cmd "$cmd" '{ AttachStdin: false, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: ["bash", "-lc", $cmd] }') create_response=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" \ -X POST \ -H "Content-Type: application/json" \ -d "$create_payload" \ "$exec_create_endpoint") exec_id=$(printf '%s' "$create_response" | jq -r '.Id // empty') if [ -z "$exec_id" ]; then log "failed to create exec for $PG_CONTAINER_NAME" log "docker response: $create_response" exit 1 fi log "starting exec $exec_id" exec_start_endpoint="${docker_api_base}/exec/${exec_id}/start" start_output=$(curl --fail --show-error --silent --unix-socket "$DOCKER_SOCKET" \ -X POST \ -H "Content-Type: application/json" \ -d '{"Detach": false, "Tty": true}' \ "$exec_start_endpoint") if [ -n "$start_output" ]; then printf '%s\n' "$start_output" | while IFS= read -r line; do log "exec output: $line" done fi exec_inspect_endpoint="${docker_api_base}/exec/${exec_id}/json" inspect_response=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" "$exec_inspect_endpoint") exit_code=$(printf '%s' "$inspect_response" | jq -r '.ExitCode // empty') if [ -z "$exit_code" ]; then log "could not determine exec exit code" log "docker inspect response: $inspect_response" exit 1 fi if [ "$exit_code" != "0" ]; then log "backup exec exited with status $exit_code" log "docker inspect response: $inspect_response" exit "$exit_code" fi if [ -f "$backup_path" ]; then size=$(du -h "$backup_path" 2>/dev/null | awk '{print $1}') if [ -n "$size" ]; then log "backup completed: $backup_path ($size)" else log "backup completed: $backup_path" fi else log "backup file not found on host; verifying inside container $PG_CONTAINER_NAME" verify_cmd="set -eo pipefail && if [ -f \"${backup_path}\" ]; then du -h \"${backup_path}\" 2>/dev/null | awk 'NR==1{print \$1}' || printf 'exists'; else exit 44; fi" verify_payload=$(jq -n --arg cmd "$verify_cmd" '{ AttachStdin: false, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: ["bash", "-lc", $cmd] }') verify_create_response=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" \ -X POST \ -H "Content-Type: application/json" \ -d "$verify_payload" \ "$exec_create_endpoint") verify_exec_id=$(printf '%s' "$verify_create_response" | jq -r '.Id // empty') if [ -z "$verify_exec_id" ]; then log "failed to create verification exec for $PG_CONTAINER_NAME" log "docker response: $verify_create_response" log "backup command succeeded, but file $backup_path not found" log "database backup finished" exit 0 fi log "starting verification exec $verify_exec_id" verify_start_endpoint="${docker_api_base}/exec/${verify_exec_id}/start" verify_start_output=$(curl --fail --show-error --silent --unix-socket "$DOCKER_SOCKET" \ -X POST \ -H "Content-Type: application/json" \ -d '{"Detach": false, "Tty": true}' \ "$verify_start_endpoint") if [ -n "$verify_start_output" ]; then printf '%s\n' "$verify_start_output" | while IFS= read -r line; do log "verify output: $line" done fi verify_inspect_endpoint="${docker_api_base}/exec/${verify_exec_id}/json" verify_inspect_response=$(curl --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" "$verify_inspect_endpoint") verify_exit_code=$(printf '%s' "$verify_inspect_response" | jq -r '.ExitCode // empty') if [ -z "$verify_exit_code" ]; then log "could not determine verification exec exit code" log "docker inspect response: $verify_inspect_response" log "backup command succeeded, but file $backup_path not found" elif [ "$verify_exit_code" = "0" ]; then verify_size=$(printf '%s' "$verify_start_output" | awk 'NF {last=$0} END {print last}') verify_size=$(printf '%s' "$verify_size" | tr -d '\r') if [ -n "$verify_size" ]; then log "backup completed inside container: $backup_path ($verify_size)" else log "backup completed inside container: $backup_path" fi elif [ "$verify_exit_code" = "44" ]; then log "backup command succeeded, but file $backup_path not found inside container $PG_CONTAINER_NAME" else log "verification exec exited with status $verify_exit_code" log "docker inspect response: $verify_inspect_response" log "backup command succeeded, but file $backup_path not confirmed" fi fi log "database backup finished"