Files
Linux/database-dump-via-docker-sock.sh

306 lines
7.5 KiB
Bash
Executable File

#!/bin/sh
set -eu
# Generic database backup triggered through the Docker Engine unix socket.
# Supports PostgreSQL, MySQL/MariaDB, and Kingbase containers.
log() {
printf '%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*"
}
print_usage() {
cat <<EOF_USAGE >&2
Usage: $0 [--socket=PATH] [--api-version=VERSION] --container=NAME \\
[--type=postgres|mysql|kingbase] --backup-prefix=PREFIX [--backup-dir=DIR] \\
[--mysql-user=USER] [--mysql-password=PASSWORD] [--exec-user=NAME]
Options:
--socket=PATH Docker Engine unix socket path (default: /var/run/docker.sock)
--api-version=VERSION Docker API version (default: v1.51)
--container=NAME Container name to execute the backup in
--type=TYPE Database type: postgres, mysql, or kingbase (auto-detected from container name if omitted)
--backup-dir=DIR Directory to store backups (default: /backups)
--backup-prefix=PREFIX Backup filename prefix
--mysql-user=USER MySQL user (default: root)
--mysql-password=PASSWORD MySQL password (optional)
--exec-user=NAME Force a specific user when running commands inside the container
--help Show this help message
EOF_USAGE
}
DOCKER_SOCKET="/var/run/docker.sock"
DOCKER_API_VERSION="v1.51"
CONTAINER_NAME=""
DB_TYPE=""
BACKUP_DIR="/backups"
BACKUP_PREFIX=""
MYSQL_USER="root"
MYSQL_PASSWORD=""
MYSQL_PASSWORD_SET=0
EXEC_USER_OVERRIDE=""
EXEC_SHELL="sh"
detect_db_type_from_name() {
name_lower=$(printf '%s' "$1" | tr 'A-Z' 'a-z')
case $name_lower in
*postgres*|*pgsql*|*pg-*)
printf 'postgres'
return 0
;;
*pg*)
printf 'postgres'
return 0
;;
*mysql*|*mariadb*|*maria*)
printf 'mysql'
return 0
;;
*kingbase*)
printf 'kingbase'
return 0
;;
esac
return 1
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
printf '%s requires %s in PATH\n' "$0" "$1" >&2
exit 1
fi
}
missing_value() {
printf 'Missing value for %s\n' "$1" >&2
print_usage
exit 1
}
escape_for_double_quotes() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/`/\\`/g; s/\$/\\$/g'
}
docker_exec() {
exec_container_name=$1
exec_cmd=$2
exec_user=${3:-}
set -- \
--socket="$DOCKER_SOCKET" \
--api-version="$DOCKER_API_VERSION" \
--container="$exec_container_name" \
--cmd="$exec_cmd"
if [ -n "$exec_user" ]; then
set -- "$@" --user="$exec_user"
fi
if [ -n "${EXEC_SHELL:-}" ]; then
set -- "$@" --shell="$EXEC_SHELL"
fi
curl -fsSL "https://Git.1-H.CC/Scripts/Linux/raw/branch/main/docker-exec-via-sock.sh" | sh -s -- "$@"
}
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=*)
CONTAINER_NAME="${1#*=}"
;;
--container)
if [ "$#" -lt 2 ]; then missing_value '--container'; fi
shift
CONTAINER_NAME="$1"
;;
--type=*)
DB_TYPE="${1#*=}"
;;
--type)
if [ "$#" -lt 2 ]; then missing_value '--type'; fi
shift
DB_TYPE="$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"
;;
--mysql-user=*)
MYSQL_USER="${1#*=}"
;;
--mysql-user)
if [ "$#" -lt 2 ]; then missing_value '--mysql-user'; fi
shift
MYSQL_USER="$1"
;;
--mysql-password=*)
MYSQL_PASSWORD="${1#*=}"
MYSQL_PASSWORD_SET=1
;;
--mysql-password)
if [ "$#" -lt 2 ]; then missing_value '--mysql-password'; fi
shift
MYSQL_PASSWORD="$1"
MYSQL_PASSWORD_SET=1
;;
--exec-user=*)
EXEC_USER_OVERRIDE="${1#*=}"
;;
--exec-user)
if [ "$#" -lt 2 ]; then missing_value '--exec-user'; fi
shift
EXEC_USER_OVERRIDE="$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
require_command curl
require_command jq
require_command sed
require_command tr
if [ -z "$DOCKER_SOCKET" ] || [ -z "$DOCKER_API_VERSION" ] || [ -z "$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
if [ -z "$DB_TYPE" ]; then
if detected_type=$(detect_db_type_from_name "$CONTAINER_NAME"); then
DB_TYPE=$detected_type
log "auto-detected database type '$DB_TYPE' from container name '$CONTAINER_NAME'"
else
printf 'Unable to detect database type from container name: %s\n' "$CONTAINER_NAME" >&2
printf 'Please specify --type=postgres, --type=mysql, or --type=kingbase\n' >&2
exit 1
fi
fi
EXEC_USER_DEFAULT=""
case $DB_TYPE in
postgres)
BACKUP_EXTENSION=".sql.zst"
DUMP_CMD="pg_dumpall --username=\"\${POSTGRES_USER:-postgres}\" --clean"
COMPRESS_CMD="zstd"
EXEC_SHELL="bash"
;;
mysql)
BACKUP_EXTENSION=".sql.zst"
mysql_user_escaped=$(escape_for_double_quotes "$MYSQL_USER")
DUMP_CMD="mysqldump --all-databases --user=\"${mysql_user_escaped}\""
if [ "$MYSQL_PASSWORD_SET" -eq 1 ]; then
mysql_password_escaped=$(escape_for_double_quotes "$MYSQL_PASSWORD")
DUMP_CMD="MYSQL_PWD=\"${mysql_password_escaped}\" $DUMP_CMD"
fi
COMPRESS_CMD="zstd"
EXEC_SHELL="sh"
;;
kingbase)
BACKUP_EXTENSION=".sql.zst"
DUMP_CMD="sys_dumpall --clean --username=\"\${DB_USER:-kingbase}\""
COMPRESS_CMD="zstd"
EXEC_USER_DEFAULT="root"
EXEC_SHELL="sh"
;;
*)
printf 'Unsupported database type: %s\n' "$DB_TYPE" >&2
exit 1
;;
esac
if [ -n "$EXEC_USER_OVERRIDE" ]; then
EXEC_USER="$EXEC_USER_OVERRIDE"
else
EXEC_USER="$EXEC_USER_DEFAULT"
fi
timestamp=$(date +%Y-%m-%d_%H-%M-%S)
backup_path="$BACKUP_DIR/${BACKUP_PREFIX}${timestamp}${BACKUP_EXTENSION}"
log "preparing $DB_TYPE backup at $backup_path via docker unix socket"
cmd="mkdir -p \"${BACKUP_DIR}\" && set -o pipefail && ${DUMP_CMD} | ${COMPRESS_CMD} > \"${backup_path}\""
log_border="----------------------------------------------------------------------"
printf '%s\n' "$log_border" >&2
if ! docker_exec "$CONTAINER_NAME" "$cmd" "${EXEC_USER:-}"; then
printf '%s\n' "$log_border" >&2
log "backup command failed"
exit 1
fi
printf '%s\n' "$log_border" >&2
printf '\n' >&2
log "backup command finished, verifying file size inside container"
verify_cmd="du -h \"${backup_path}\" 2>/dev/null | awk 'NR==1{print \$1}'"
printf '%s\n' "$log_border" >&2
size=$(docker_exec "$CONTAINER_NAME" "$verify_cmd" "${EXEC_USER:-}" | tr -d '\r')
printf '%s\n' "$log_border" >&2
printf '\n' >&2
if [ -n "$size" ]; then
log "backup completed inside container: $backup_path ($size)"
else
log "backup completed, but could not determine file size inside container."
fi
printf '\n' >&2
log "database backup finished"