|
#!/bin/bash |
|
|
|
# --- Configuration --- |
|
NEW_DATA_ROOT="" # This will be set by user input |
|
MAX_VERIFY_RETRIES=5 # How many times to retry checking docker info |
|
VERIFY_RETRY_DELAY=3 # How long to wait between retries (seconds) |
|
|
|
# --- Functions --- |
|
|
|
# Function to check for root privileges |
|
check_root() { |
|
if [ "$EUID" -ne 0 ]; then |
|
echo "Error: This script must be run as root (using sudo)." |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Function to get user input for the new data root |
|
get_new_data_root() { |
|
read -rp "Enter the FULL path to the directory on your mounted external drive where Docker data should be stored (e.g., /mnt/mydrive/docker-data): " NEW_DATA_ROOT |
|
|
|
# Basic validation |
|
if [ -z "$NEW_DATA_ROOT" ]; then |
|
echo "Error: Path cannot be empty." |
|
exit 1 |
|
fi |
|
|
|
# Resolve absolute path in case user used relative path or ~/ |
|
# Need non-root user's home directory if they use ~ |
|
if [[ "$NEW_DATA_ROOT" == "~"* ]]; then |
|
NON_ROOT_USER=$(logname 2>/dev/null || echo "$SUDO_USER") |
|
if [ -z "$NON_ROOT_USER" ]; then |
|
echo "Warning: Could not determine the non-root user. '~' might not expand correctly." |
|
else |
|
# Replace ~ with the actual home directory of the non-root user |
|
USER_HOME=$(getent passwd "$NON_ROOT_USER" | cut -d: -f6) |
|
NEW_DATA_ROOT="${NEW_DATA_ROOT/\~/$USER_HOME}" |
|
fi |
|
fi |
|
|
|
NEW_DATA_ROOT=$(realpath "$NEW_DATA_ROOT") |
|
|
|
echo "You entered: $NEW_DATA_ROOT" |
|
read -rp "Is this correct? (y/n): " confirm |
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then |
|
echo "Operation cancelled by user." |
|
exit 1 |
|
fi |
|
|
|
# Check if the path exists and is a directory, create if not |
|
if [ ! -d "$NEW_DATA_ROOT" ]; then |
|
echo "Creating directory: $NEW_DATA_ROOT" |
|
if ! mkdir -p "$NEW_DATA_ROOT"; then |
|
echo "Error: Failed to create directory '$NEW_DATA_ROOT'. Check permissions." |
|
exit 1 |
|
fi |
|
fi |
|
|
|
# Check if the path is mounted (optional but good practice) |
|
# This is a basic check, not foolproof |
|
if ! findmnt -n "$NEW_DATA_ROOT" > /dev/null; then |
|
echo "Warning: The specified path '$NEW_DATA_ROOT' does not appear to be a mount point according to 'findmnt'." |
|
echo "Ensure your external drive is correctly mounted at this path and configured to mount automatically on boot (e.g., using /etc/fstab)." |
|
read -rp "Continue anyway? (y/n): " continue_anyway |
|
if [[ ! "$continue_anyway" =~ ^[Yy]$ ]]; then |
|
echo "Operation cancelled by user." |
|
exit 1 |
|
fi |
|
fi |
|
echo "" |
|
} |
|
|
|
# Function to install necessary packages |
|
install_prerequisites() { |
|
echo "--- Installing prerequisites ---" |
|
if ! apt update; then |
|
echo "Warning: apt update failed, prerequisites installation might fail. Continuing..." |
|
fi |
|
|
|
# Add rsync and jq to prerequisites |
|
if ! apt install -y apt-transport-https ca-certificates curl gnupg lsb-release jq rsync; then |
|
echo "Error: Failed to install prerequisites (apt-transport-https, ca-certificates, curl, gnupg, lsb-release, jq, rsync)." |
|
echo "Check your internet connection and package sources." |
|
exit 1 |
|
fi |
|
echo "Prerequisites installed." |
|
echo "" |
|
} |
|
|
|
# Function to install Docker using the official repository |
|
install_docker() { |
|
echo "--- Installing Docker ---" |
|
|
|
# Check if Docker is already installed via apt |
|
if dpkg -s docker-ce >/dev/null 2>&1; then |
|
echo "Docker (docker-ce) is already installed via apt. Skipping installation steps." |
|
# Ensure containerd.io is also installed if docker-ce is already there |
|
if ! dpkg -s containerd.io >/dev/null 2>&1; then |
|
echo "containerd.io not found, installing..." |
|
if ! apt install -y containerd.io; then |
|
echo "Error: Failed to install containerd.io." |
|
exit 1 |
|
fi |
|
fi |
|
return 0 # Exit function successfully |
|
fi |
|
|
|
echo "Docker not found or not installed via apt. Proceeding with official repository setup." |
|
|
|
# Add Docker's official GPG key directory if it doesn't exist (for newer apt versions) |
|
mkdir -m 0755 -p /etc/apt/keyrings |
|
|
|
# Add Docker's official GPG key |
|
if ! command -v curl > /dev/null; then echo "Error: curl is required but not installed."; exit 1; fi |
|
if ! command -v gpg > /dev/null; then echo "Error: gpg is required but not installed."; exit 1; fi |
|
|
|
echo "Adding Docker GPG key..." |
|
if ! curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg; then |
|
echo "Error: Failed to download or add Docker GPG key." |
|
exit 1 |
|
fi |
|
chmod a+r /etc/apt/keyrings/docker.gpg # Recommended permissions for the keyring file |
|
echo "GPG key added." |
|
|
|
# Add Docker repository |
|
echo "Adding Docker repository..." |
|
DISTRO_CODENAME=$(lsb_release -cs) |
|
echo \ |
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ |
|
"$DISTRO_CODENAME" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null |
|
echo "Repository added." |
|
|
|
# Install Docker packages |
|
echo "Updating apt package list..." |
|
if ! apt update; then |
|
echo "Error: Failed to update apt package list after adding repository." |
|
exit 1 |
|
fi |
|
|
|
echo "Installing docker-ce, docker-ce-cli, containerd.io..." |
|
if ! apt install -y docker-ce docker-ce-cli containerd.io; then |
|
echo "Error: Failed to install Docker packages. See above error messages." |
|
echo "If Docker was previously installed via snap or another method, you might need to remove it first." |
|
echo "Attempting to install without containerd.io as a fallback..." |
|
# Fallback attempt without containerd.io |
|
if ! apt install -y docker-ce docker-ce-cli; then |
|
echo "Error: Failed to install docker-ce or docker-ce-cli even in fallback." |
|
exit 1 |
|
fi |
|
echo "docker-ce and docker-ce-cli installed, containerd.io might be missing." |
|
fi |
|
echo "Docker packages installed successfully." |
|
echo "" |
|
} |
|
|
|
# Function to configure Docker data root and move existing data |
|
configure_data_root() { |
|
echo "--- Configuring Docker Data Root ---" |
|
|
|
# Stop Docker if running |
|
echo "Stopping Docker service..." |
|
# systemctl stop docker will return non-zero if not running, use || true to ignore |
|
systemctl stop docker || true |
|
# Also stop containerd, as it's the underlying runtime |
|
systemctl stop containerd || true |
|
# Allow a moment for processes to stop |
|
sleep 5 # Increased sleep slightly |
|
|
|
# Check if original data directory exists and has content relevant to docker |
|
# Checking for specific subdirectories is better than just ls -A |
|
if [ -d "/var/lib/docker" ] && [ -d "/var/lib/docker/containers" ] || [ -d "/var/lib/docker/image" ] || [ -d "/var/lib/docker/volumes" ]; then |
|
echo "Existing Docker data found in /var/lib/docker. Moving to $NEW_DATA_ROOT..." |
|
|
|
# Use rsync for safer data transfer |
|
# -a: archive mode (preserves permissions, ownership, timestamps, symlinks) |
|
# -P: show progress |
|
# --remove-source-files: Remove files from source AFTER they are successfully transferred |
|
# --ignore-existing: Important if script was run before and partially moved data |
|
if ! rsync -aP --remove-source-files /var/lib/docker/ "$NEW_DATA_ROOT/"; then |
|
echo "Error: Failed to move existing data with rsync." |
|
echo "Your original data in /var/lib/docker might be incomplete or partially moved." |
|
echo "Investigate the rsync error. Data in $NEW_DATA_ROOT might also be incomplete." |
|
echo "It is strongly recommended to restore from backup if you have one." |
|
exit 1 |
|
fi |
|
echo "Data moved successfully to $NEW_DATA_ROOT." |
|
|
|
# Verify original directory is now empty or mostly empty |
|
if [ "$(ls -A /var/lib/docker/)" ]; then |
|
echo "Warning: Original directory /var/lib/docker/ is NOT empty after rsync with --remove-source-files." |
|
echo "This might indicate permission issues or files that were in use." |
|
echo "Please check /var/lib/docker/ manually." |
|
else |
|
echo "Original directory /var/lib/docker/ appears to be empty." |
|
fi |
|
|
|
else |
|
echo "No significant existing Docker data found in /var/lib/docker. No data to move." |
|
fi |
|
|
|
# Create or modify daemon.json using jq |
|
DAEMON_JSON_PATH="/etc/docker/daemon.json" |
|
TMP_DAEMON_JSON="/tmp/daemon.json.tmp.$$" # Use $$ for unique temporary file |
|
|
|
echo "Creating or updating $DAEMON_JSON_PATH with data-root: $NEW_DATA_ROOT" |
|
mkdir -p /etc/docker |
|
|
|
# Check if the file exists and if "data-root" is already set correctly |
|
DATA_ROOT_ALREADY_SET=false |
|
if [ -f "$DAEMON_JSON_PATH" ]; then |
|
echo "Existing $DAEMON_JSON_PATH found." |
|
# jq -e returns 0 if match is found, 1 if not |
|
if jq -e ".\"data-root\" == \"$NEW_DATA_ROOT\"" "$DAEMON_JSON_PATH" > /dev/null 2>&1; then |
|
echo "Docker data-root is already set to $NEW_DATA_ROOT in $DAEMON_JSON_PATH." |
|
DATA_ROOT_ALREADY_SET=true |
|
else |
|
# Add or update the data-root key |
|
if ! jq --indent 4 ". += {\"data-root\": \"$NEW_DATA_ROOT\"}" "$DAEMON_JSON_PATH" > "$TMP_DAEMON_JSON"; then |
|
echo "Error: Failed to modify $DAEMON_JSON_PATH using jq. Check JSON syntax if file existed." |
|
exit 1 |
|
fi |
|
fi |
|
else |
|
echo "$DAEMON_JSON_PATH not found. Creating a new file." |
|
# Create a new daemon.json with only data-root |
|
if ! echo '{}' | jq --indent 4 ". += {\"data-root\": \"$NEW_DATA_ROOT\"}" > "$TMP_DAEMON_JSON"; then |
|
echo "Error: Failed to create $DAEMON_JSON_PATH using jq." |
|
exit 1 |
|
fi |
|
fi |
|
|
|
# Move temporary file to final location only if we actually created/modified it |
|
if [ "$DATA_ROOT_ALREADY_SET" != true ]; then |
|
if mv "$TMP_DAEMON_JSON" "$DAEMON_JSON_PATH"; then |
|
echo "$DAEMON_JSON_PATH updated successfully." |
|
else |
|
echo "Error: Failed to move temporary config file to $DAEMON_JSON_PATH." |
|
exit 1 |
|
fi |
|
else |
|
# Clean up temp file if it wasn't moved |
|
rm -f "$TMP_DAEMON_JSON" |
|
fi |
|
echo "" |
|
} |
|
|
|
# Function to start Docker and verify configuration (with retries) |
|
verify_and_start_docker() { |
|
echo "--- Starting and Verifying Docker ---" |
|
|
|
echo "Reloading systemd daemon..." |
|
systemctl daemon-reload |
|
|
|
echo "Starting containerd service..." |
|
# systemctl start can sometimes succeed even if containerd fails, check both |
|
if ! systemctl start containerd; then |
|
echo "Error: Failed to start containerd service." |
|
echo "Check 'systemctl status containerd' and 'journalctl -xe' for details." |
|
# Don't exit immediately, docker might start without it depending on setup, but warn |
|
echo "Warning: containerd failed to start. Docker might not function correctly." |
|
fi |
|
|
|
echo "Starting Docker service..." |
|
if ! systemctl start docker; then |
|
echo "Error: Failed to start Docker service." |
|
echo "Check 'systemctl status docker' and 'journalctl -xe' for details." |
|
exit 1 # This is a critical failure, exit |
|
fi |
|
echo "Docker service started successfully." |
|
|
|
echo "Checking Docker info (will retry up to $MAX_VERIFY_RETRIES times)..." |
|
local retries=0 |
|
local verification_successful=false |
|
local ACTUAL_DATA_ROOT="" |
|
local docker_info_output="" |
|
|
|
while [ "$retries" -lt "$MAX_VERIFY_RETRIES" ]; do |
|
retries=$((retries + 1)) |
|
echo "Attempt $retries of $MAX_VERIFY_RETRIES..." |
|
|
|
# Run docker info and capture output and status |
|
docker_info_output=$(docker info 2>&1) |
|
local docker_info_status=$? |
|
|
|
if [ "$docker_info_status" -eq 0 ]; then |
|
# docker info succeeded, now grep the output in memory |
|
ACTUAL_DATA_ROOT=$(echo "$docker_info_output" | grep -i "Data Root Dir:" | awk '{print $NF}') |
|
|
|
if [ -n "$ACTUAL_DATA_ROOT" ] && [ "$ACTUAL_DATA_ROOT" == "$NEW_DATA_ROOT" ]; then |
|
echo "Verification successful! Docker Data Root is correctly set to: $ACTUAL_DATA_ROOT" |
|
verification_successful=true |
|
break # Exit the retry loop |
|
elif [ -n "$ACTUAL_DATA_ROOT" ]; then |
|
echo "Info: Data Root Dir found ('$ACTUAL_DATA_ROOT'), but path doesn't match expected ('$NEW_DATA_ROOT'). Retrying..." |
|
else |
|
echo "Info: 'Data Root Dir:' not found in docker info output yet. Retrying..." |
|
fi |
|
else |
|
echo "Warning: 'docker info' command failed (exit status $docker_info_status). Is the service fully initialized? Retrying..." |
|
# Optionally show the failing output |
|
# echo "$docker_info_output" |
|
fi |
|
|
|
if [ "$retries" -lt "$MAX_VERIFY_RETRIES" ]; then |
|
sleep "$VERIFY_RETRY_DELAY" # Only sleep if we are going to retry |
|
fi |
|
|
|
done # End of retry loop |
|
|
|
# After the loop, check if verification passed |
|
if [ "$verification_successful" == true ]; then |
|
echo "Docker Data Root configuration verified." |
|
else |
|
echo "Verification failed after multiple attempts." |
|
echo "Docker Data Root is still reported as '$ACTUAL_DATA_ROOT' (expected '$NEW_DATA_ROOT') or not found in 'docker info' output." |
|
echo "Check /etc/docker/daemon.json and 'systemctl status docker' / 'journalctl -xe' for errors." |
|
# Added: Log the final docker info output for debugging if verification failed |
|
echo "Last 'docker info' output:" |
|
echo "--- Start docker info output ---" |
|
echo "$docker_info_output" |
|
echo "--- End docker info output ---" |
|
echo "Continuing with adding user to docker group, but investigate the verification warning." |
|
fi |
|
echo "" |
|
} |
|
|
|
# Function to add the non-root user to the docker group |
|
add_user_to_docker_group() { |
|
echo "--- Adding User to Docker Group ---" |
|
|
|
# Get the user who ran the script via sudo |
|
NON_ROOT_USER=$(logname 2>/dev/null || echo "$SUDO_USER") |
|
|
|
if [ -z "$NON_ROOT_USER" ]; then |
|
echo "Warning: Could not determine the non-root user who ran sudo." |
|
echo "You will need to manually add your user to the 'docker' group." |
|
echo "Example: sudo usermod -aG docker <your_username>" |
|
else |
|
echo "Adding user '$NON_ROOT_USER' to the 'docker' group..." |
|
# Check if group exists |
|
if ! getent group docker >/dev/null; then |
|
echo "Warning: 'docker' group does not exist. Skipping adding user." |
|
else |
|
# Check if user is already a member |
|
if groups "$NON_ROOT_USER" | grep -q '\bdocker\b'; then |
|
echo "User '$NON_ROOT_USER' is already a member of the 'docker' group." |
|
else |
|
if usermod -aG docker "$NON_ROOT_USER"; then |
|
echo "User '$NON_ROOT_USER' successfully added to the 'docker' group." |
|
echo "You will need to log out and log back in (or restart your session) for this change to take effect." |
|
echo "After logging back in, you should be able to run 'docker ps' without using sudo." |
|
else |
|
echo "Error: Failed to add user '$NON_ROOT_USER' to the 'docker' group." |
|
echo "You may need to add them manually: sudo usermod -aG docker $NON_ROOT_USER" |
|
fi |
|
fi |
|
fi |
|
fi |
|
echo "" |
|
} |
|
|
|
|
|
# --- Main Script Execution --- |
|
|
|
check_root |
|
get_new_data_root |
|
install_prerequisites |
|
install_docker |
|
configure_data_root |
|
verify_and_start_docker |
|
add_user_to_docker_group |
|
|
|
echo "Script finished." |
|
echo "-----------------------------------------------------" |
|
echo "IMPORTANT: If user '$NON_ROOT_USER' was added to the 'docker' group," |
|
echo " you MUST log out and log back in for the changes to apply." |
|
echo "" |
|
echo "Remember to ensure your external drive is mounted at '$NEW_DATA_ROOT'" |
|
echo "automatically on boot (e.g., using /etc/fstab)." |
|
echo "-----------------------------------------------------" |
|
|
|
|
|
exit 0 # Successful script completion |