From 1747370ae841da6f2d563c11126a57285994dfdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E6=B5=A9?= Date: Mon, 22 Sep 2025 18:41:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(postgres-dump-zstd-docker-sock):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=80=9A=E8=BF=87=20Docker=20unix=20socket?= =?UTF-8?q?=20=E8=A7=A6=E5=8F=91=E7=9A=84=20PostgreSQL=20=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- postgres-dump-zstd-docker-sock.sh | 196 ++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100755 postgres-dump-zstd-docker-sock.sh diff --git a/postgres-dump-zstd-docker-sock.sh b/postgres-dump-zstd-docker-sock.sh new file mode 100755 index 0000000..edbec1f --- /dev/null +++ b/postgres-dump-zstd-docker-sock.sh @@ -0,0 +1,196 @@ +#!/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}.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 command succeeded, but file $backup_path not found" +fi + +log "database backup finished"