This article provides an enhanced Docker data migration script with strict pre-checks, safety validations, and automatic dependency installation.
Recommended Method: Run Online Script
The easiest way is to run the script online without downloading manually:
bash <(curl -sSL https://reshub.cn/data/sh/docker-move.sh)
Tip: Ensure you have root privileges and sufficient disk space before running.
Migration Steps
- Stop the Docker service
- Perform pre-checks and validate the new path
- Migrate data with rsync while preserving attributes
- Backup the old directory
- Update
/etc/docker/daemon.json
configuration - Restart Docker and verify the migration
Full Script (for reference)
#!/bin/bash # Docker data migration script (universal version, supports CentOS7/8/9, Debian/Ubuntu, Alpine) # Usage: sudo ./docker-move.sh /data1/docker # # Source (GitHub): https://github.com/reshub-cn/docker-data-move.sh # Author: reshub-cn # Usage example: sudo ./docker-move.sh /data1/docker set -euo pipefail NEW_PATH=${1:-} DOCKER_SERVICE="docker" DOCKER_DIR="/var/lib/docker" CONFIG_FILE="/etc/docker/daemon.json" # Allow migration to a non-empty directory (default 0 = not allowed). # To override temporarily: # ALLOW_NONEMPTY=1 sudo ./docker-move.sh /path ALLOW_NONEMPTY="${ALLOW_NONEMPTY:-0}" # ----------- Output functions ----------- die() { echo -e "\n[ERROR] $*\n" >&2; exit 1; } info() { echo "[INFO] $*"; } warn() { echo "[WARN] $*"; } # ----------- Validation functions ----------- require_root() { [[ "${EUID:-$(id -u)}" -eq 0 ]] || die "Please run as root (sudo)." } require_new_path() { [[ -n "$NEW_PATH" ]] || die "Please provide the new Docker data directory. Usage: sudo $0 /data1/docker" [[ "$NEW_PATH" == /* ]] || die "New path must be an absolute path: $NEW_PATH" [[ -d "$DOCKER_DIR" ]] || die "Old directory does not exist: $DOCKER_DIR. No standard Docker installation detected." if [[ "$NEW_PATH" == "$DOCKER_DIR" ]]; then die "New directory cannot be the same as the current one: $NEW_PATH" fi if [[ "$NEW_PATH" == "$DOCKER_DIR"* ]]; then die "New directory cannot be inside the old one: $NEW_PATH is within $DOCKER_DIR" fi if [[ "$DOCKER_DIR" == "$NEW_PATH"* ]]; then die "Old directory cannot be inside the new one: $DOCKER_DIR is within $NEW_PATH" fi mkdir -p "$NEW_PATH" || die "Failed to create new directory: $NEW_PATH" chown root:root "$NEW_PATH" || die "Failed to set owner for new directory: $NEW_PATH" if [[ "$ALLOW_NONEMPTY" != "1" ]]; then if [[ -d "$NEW_PATH" ]] && [[ -n "$(ls -A "$NEW_PATH" 2>/dev/null || true)" ]]; then die "New directory must be empty (or set ALLOW_NONEMPTY=1 to override): $NEW_PATH" fi fi } require_cmds() { command -v docker >/dev/null 2>&1 || die "Docker command not found. Please install Docker first." if ! command -v rsync >/dev/null 2>&1; then warn "rsync not found, attempting to install..." if [[ -f /etc/debian_version ]]; then apt update && apt install -y rsync || true elif [[ -f /etc/redhat-release ]]; then yum install -y rsync || dnf install -y rsync || true elif [[ -f /etc/alpine-release ]]; then apk add --no-cache rsync || true fi fi command -v rsync >/dev/null 2>&1 || die "Failed to install rsync, please install it manually and retry." } check_space() { local used avail need parent used=$(du -sb "$DOCKER_DIR" 2>/dev/null | awk '{print $1}') [[ -n "$used" && "$used" -gt 0 ]] || die "Failed to determine disk usage for $DOCKER_DIR." parent="$NEW_PATH" [[ -d "$parent" ]] || parent="$(dirname "$NEW_PATH")" avail=$(df -P -B1 "$parent" 2>/dev/null | awk 'NR==2{print $4}') [[ -n "$avail" && "$avail" -gt 0 ]] || die "Failed to determine available space on $parent." local GiB2=$((2*1024*1024*1024)) local need1=$(( (used * 110 + 99) / 100 )) # 110% rounded up local need2=$(( used + GiB2 )) need=$(( need1 > need2 ? need1 : need2 )) info "Old directory usage: $used bytes; Target available: $avail bytes; Required: $need bytes" [[ "$avail" -ge "$need" ]] || die "Insufficient disk space (required: $need, available: $avail)." } check_selinux() { if command -v getenforce >/dev/null 2>&1; then local mode mode=$(getenforce 2>/dev/null || echo "") if [[ "$mode" == "Enforcing" ]]; then cat >&2 </dev/null 2>&1; then if ! jq -e '.' "$CONFIG_FILE" >/dev/null 2>&1; then local bak="${CONFIG_FILE}.bak.$(date +%Y%m%d%H%M%S)" cp -a "$CONFIG_FILE" "$bak" || true die "Invalid JSON detected in $CONFIG_FILE. Backup saved to: $bak" fi else warn "jq not installed, cannot validate JSON format of $CONFIG_FILE." fi fi } preflight_checks() { info "Running preflight checks..." require_root require_cmds require_new_path check_space check_selinux check_daemon_json info "Preflight checks passed ✅" } # ----------- Docker control functions ----------- stop_docker() { if command -v systemctl &>/dev/null; then systemctl stop "$DOCKER_SERVICE" || true systemctl stop "${DOCKER_SERVICE}.socket" || true elif command -v service &>/dev/null; then service "$DOCKER_SERVICE" stop || true else die "Neither systemctl nor service found, cannot stop Docker automatically." fi } start_docker() { if command -v systemctl &>/dev/null; then systemctl daemon-reexec || true systemctl start "$DOCKER_SERVICE" elif command -v service &>/dev/null; then service "$DOCKER_SERVICE" start else die "Neither systemctl nor service found, cannot start Docker automatically." fi } # ----------- Main workflow ----------- echo "Starting Docker data migration to: $NEW_PATH" # Auto-install jq (fix for CentOS7) if ! command -v jq &>/dev/null; then echo "jq not installed, attempting to install..." if [[ -f /etc/debian_version ]]; then apt update && apt install -y jq || true elif [[ -f /etc/redhat-release ]]; then yum install -y epel-release || true rpm --import https://archive.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-7 || true yum install -y jq oniguruma || dnf install -y jq oniguruma || true elif [[ -f /etc/alpine-release ]]; then apk add --no-cache jq || true fi fi # 0. Preflight checks preflight_checks # 1. Stop Docker echo "Stopping Docker service..." stop_docker # 2. Ensure new path exists echo "Ensuring new directory exists..." mkdir -p "$NEW_PATH" chown root:root "$NEW_PATH" # 3. Migrate data echo "Migrating data..." rsync -aHAX --numeric-ids --delete --info=progress2 "$DOCKER_DIR/" "$NEW_PATH/" # 4. Backup old directory if [[ -d "$DOCKER_DIR" ]]; then echo "Backing up old directory..." mv "$DOCKER_DIR" "${DOCKER_DIR}.bak.$(date +%Y%m%d%H%M%S)" fi # 5. Update Docker config echo "Updating Docker config..." mkdir -p "$(dirname "$CONFIG_FILE")" if [[ -f "$CONFIG_FILE" && command -v jq >/dev/null 2>&1 ]]; then tmp="${CONFIG_FILE}.tmp" # Use --arg to safely pass path (handles spaces/special chars) if ! jq --arg path "$NEW_PATH" '.["data-root"]=$path' "$CONFIG_FILE" > "$tmp" 2>/dev/null; then bak="${CONFIG_FILE}.bak.$(date +%Y%m%d%H%M%S)" cp -a "$CONFIG_FILE" "$bak" || true echo '{"data-root":"'$NEW_PATH'"}' > "$tmp" warn "Failed to update $CONFIG_FILE, backup saved to $bak. Overwritten with minimal config." fi mv "$tmp" "$CONFIG_FILE" else echo '{"data-root":"'$NEW_PATH'"}' > "$CONFIG_FILE" fi # 6. Start Docker echo "Starting Docker..." start_docker # 7. Verify echo "Verifying Docker data directory..." if docker info >/dev/null 2>&1; then docker info | grep -E "Docker Root Dir:\s+$NEW_PATH" >/dev/null 2>&1 \ && echo "Verification successful: Docker Root Dir is $NEW_PATH" \ || die "Verification failed: Docker Root Dir not switched to $NEW_PATH, please check." else die "docker info failed, please check if Docker is running correctly." fi echo "Migration completed! Old data (if any) backed up to: ${DOCKER_DIR}.bak.*"