tool.tl Minimal Tools · Maximum Value

How to Migrate Docker Data Directory to a New Path (with Automation Script)

Category: Scripts · Published on 2025-09-12

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

  1. Stop the Docker service
  2. Perform pre-checks and validate the new path
  3. Migrate data with rsync while preserving attributes
  4. Backup the old directory
  5. Update /etc/docker/daemon.json configuration
  6. 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.*"

👁 Views: 151
👎 Dislike 0