Skip to content

Instantly share code, notes, and snippets.

@kaigouthro
Last active May 21, 2025 01:17
Show Gist options
  • Save kaigouthro/6db30f67764b70ee84b73cb03ea776f2 to your computer and use it in GitHub Desktop.
Save kaigouthro/6db30f67764b70ee84b73cb03ea776f2 to your computer and use it in GitHub Desktop.
Install Docker to Mounted Drive

A Bash script for Debian/Ubuntu-based systems that will install Docker (if not already installed) and configure it to use a specified directory (presumably on your mounted external drive) for storing containers, images, volumes, etc.

Important Considerations BEFORE Running:

  1. External Drive MUST be Mounted: This script assumes your external drive is already mounted before you run the script. It does not handle mounting the drive itself.
  2. Persistent Mount: For Docker to start correctly after a reboot, the external drive must be mounted persistently (e.g., via an entry in /etc/fstab) at the same path every time.
  3. Backup: If you have existing Docker data in /var/lib/docker and it's important, BACK IT UP before running this script. The script attempts to move existing data, but failures can happen.
  4. Root Privileges: You need to run this script with sudo.
  5. Distribution: This script is tailored for Debian/Ubuntu. The package manager (apt), repository setup, and service management (systemctl) commands are specific to these distributions. It won't work directly on Fedora, CentOS, Arch, etc.
  6. Target Directory: The target directory you specify should be empty if possible, or be prepared to receive the contents of /var/lib/docker.

How to Use:

  1. Save: Save the code above into a file, for example, install_docker_external.sh.
  2. Make Executable: Open a terminal and make the script executable:
chmod +x install_docker_external.sh
  1. Mount Drive: Ensure your external drive is mounted at the desired location.
  2. Run: Execute the script using sudo:
sudo bash install_docker_external.sh
  1. Follow Prompts: The script will ask you for the full path to the directory on your external drive. Enter the path and confirm.
  2. Complete Setup: After the script finishes, follow the instructions it prints regarding adding your user to the docker group.

This script combines the installation steps with the configuration steps, including stopping Docker, moving potential existing data, updating the configuration file, and restarting/verifying the service.

#!/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment