#!/usr/bin/env bash # # install-docker.sh — Detect or install Docker + Docker Compose plugin # Production-ready. Idempotent. Verifies every step. # set -o pipefail # ---------- Colors ---------- if [[ -t 1 ]]; then C_RESET=$'\e[0m' C_ORANGE=$'\e[38;5;208m' C_GREEN=$'\e[32m' C_RED=$'\e[31m' C_CYAN=$'\e[36m' C_BOLD=$'\e[1m' else C_RESET='' C_ORANGE='' C_GREEN='' C_RED='' C_CYAN='' C_BOLD='' fi proc() { printf "%s[PROC]%s %s\n" "$C_ORANGE" "$C_RESET" "$*"; } ok() { printf "%s[ OK ]%s %s\n" "$C_GREEN" "$C_RESET" "$*"; } fail() { printf "%s[FAIL]%s %s\n" "$C_RED" "$C_RESET" "$*" >&2; } info() { printf "%s[INFO]%s %s\n" "$C_CYAN" "$C_RESET" "$*"; } die() { fail "$*"; exit 1; } # ---------- Banner ---------- banner() { clear printf "%s%s" "$C_CYAN" "$C_BOLD" cat <<'EOF' ___ ___ _ |_ _|_ __ ___/ _ \ _ __ ___ _ __ (_) __ _ | || '_ \/ __| | | | '_ ` _ \| '_ \ | |/ _` | | || | | \__ \ |_| | | | | | | | | || | (_| | |___|_| |_|___/\___/|_| |_| |_|_| |_||_|\__,_| EOF cat </dev/null 2>&1; then SUDO="sudo" ok "sudo available — will use for privileged operations" else die "Not root and sudo not installed. Install sudo or run as root." fi } # ---------- Dependency check ---------- ensure_dep() { local cmd=$1 pkg=${2:-$1} if command -v "$cmd" >/dev/null 2>&1; then ok "Dependency present: $cmd" return 0 fi proc "Installing missing dependency: $pkg" case "$PKG_MGR" in apt) $SUDO apt-get update -qq && $SUDO apt-get install -y -qq "$pkg" ;; dnf) $SUDO dnf install -y -q "$pkg" ;; yum) $SUDO yum install -y -q "$pkg" ;; pacman) $SUDO pacman -Sy --noconfirm --needed "$pkg" ;; zypper) $SUDO zypper --non-interactive install "$pkg" ;; apk) $SUDO apk add --quiet "$pkg" ;; *) die "Unknown package manager; cannot install $pkg" ;; esac command -v "$cmd" >/dev/null 2>&1 || die "Failed to install $pkg" ok "Installed: $pkg" } # ---------- OS detection ---------- detect_os() { proc "Detecting operating system..." [[ "$(uname -s)" == "Linux" ]] || die "This script supports Linux only. For macOS/Windows use Docker Desktop." [[ -r /etc/os-release ]] || die "/etc/os-release not found — unsupported distribution." # shellcheck disable=SC1091 . /etc/os-release OS_ID="${ID:-unknown}" OS_LIKE="${ID_LIKE:-}" OS_VER="${VERSION_ID:-}" OS_CODENAME="${VERSION_CODENAME:-}" OS_NAME="${PRETTY_NAME:-$OS_ID}" case "$OS_ID $OS_LIKE" in *debian*|*ubuntu*) PKG_MGR="apt" ; OS_FAMILY="debian" ;; *rhel*|*fedora*|*centos*|*rocky*|*almalinux*) if command -v dnf >/dev/null 2>&1; then PKG_MGR="dnf" else PKG_MGR="yum"; fi OS_FAMILY="rhel" ;; *arch*) PKG_MGR="pacman"; OS_FAMILY="arch" ;; *suse*|*opensuse*) PKG_MGR="zypper"; OS_FAMILY="suse" ;; *alpine*) PKG_MGR="apk" ; OS_FAMILY="alpine" ;; *) die "Unsupported distribution: $OS_ID" ;; esac ok "OS: $OS_NAME (family: $OS_FAMILY, pkg: $PKG_MGR)" } # ---------- Docker check ---------- check_docker() { proc "Checking if Docker is installed..." if ! command -v docker >/dev/null 2>&1; then info "Docker is NOT installed." return 1 fi local ver ver=$(docker --version 2>/dev/null) || { fail "docker binary present but not working"; return 1; } ok "Docker binary found: $ver" proc "Verifying Docker daemon..." if $SUDO docker info >/dev/null 2>&1; then ok "Docker daemon is running and reachable" else fail "Docker installed but daemon not reachable" return 1 fi proc "Checking Docker Compose plugin..." if $SUDO docker compose version >/dev/null 2>&1; then ok "Docker Compose: $($SUDO docker compose version 2>/dev/null)" else info "Docker Compose plugin missing — will install" return 2 fi return 0 } # ---------- Check for updates to Docker packages ---------- # Populates global array UPDATE_PKGS with names of Docker-related packages that have pending updates. UPDATE_PKGS=() check_docker_updates() { UPDATE_PKGS=() proc "Checking for available updates to Docker components..." local -a watch=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \ docker docker-compose docker-cli-compose docker-buildx) case "$PKG_MGR" in apt) $SUDO apt-get update -qq >/dev/null 2>&1 || { fail "apt-get update failed"; return 1; } local line pkg while IFS= read -r line; do pkg="${line%%/*}" for w in "${watch[@]}"; do [[ "$pkg" == "$w" ]] && UPDATE_PKGS+=("$pkg") && break done done < <(apt list --upgradable 2>/dev/null | tail -n +2) ;; dnf|yum) local out out=$($SUDO $PKG_MGR -q check-update 2>/dev/null || true) for w in "${watch[@]}"; do grep -q "^${w}\." <<<"$out" && UPDATE_PKGS+=("$w") done ;; pacman) $SUDO pacman -Sy >/dev/null 2>&1 || true local out out=$(pacman -Qu 2>/dev/null || true) for w in "${watch[@]}"; do grep -q "^${w} " <<<"$out" && UPDATE_PKGS+=("$w") done ;; zypper) local out out=$($SUDO zypper -q list-updates 2>/dev/null || true) for w in "${watch[@]}"; do grep -q " ${w} " <<<"$out" && UPDATE_PKGS+=("$w") done ;; apk) $SUDO apk update -q >/dev/null 2>&1 || true local out out=$(apk version -l '<' 2>/dev/null | tail -n +2 || true) for w in "${watch[@]}"; do grep -q "^${w}-" <<<"$out" && UPDATE_PKGS+=("$w") done ;; esac if ((${#UPDATE_PKGS[@]} == 0)); then ok "All Docker components are up to date." return 0 fi info "Updates available for the following Docker components:" local p for p in "${UPDATE_PKGS[@]}"; do printf " %s•%s %s\n" "$C_ORANGE" "$C_RESET" "$p" done return 0 } # ---------- Apply updates to the listed Docker packages ---------- apply_docker_updates() { ((${#UPDATE_PKGS[@]} > 0)) || return 0 proc "Updating Docker components: ${UPDATE_PKGS[*]}" case "$PKG_MGR" in apt) $SUDO apt-get install -y -qq --only-upgrade "${UPDATE_PKGS[@]}" ;; dnf) $SUDO dnf upgrade -y -q "${UPDATE_PKGS[@]}" ;; yum) $SUDO yum update -y -q "${UPDATE_PKGS[@]}" ;; pacman) $SUDO pacman -S --noconfirm --needed "${UPDATE_PKGS[@]}" ;; zypper) $SUDO zypper --non-interactive update "${UPDATE_PKGS[@]}" ;; apk) $SUDO apk upgrade "${UPDATE_PKGS[@]}" ;; esac || { fail "Update failed"; return 1; } proc "Verifying Docker still works after update..." $SUDO docker info >/dev/null 2>&1 || die "Docker daemon broken after update" $SUDO docker compose version >/dev/null 2>&1 || die "Docker Compose broken after update" ok "Docker updated successfully: $(docker --version)" ok "Compose: $($SUDO docker compose version)" } # ---------- OS security updates (NEVER distro upgrade) ---------- os_update() { proc "Fetching available OS package updates (security + regular — NO distro upgrade)..." local rc=0 case "$PKG_MGR" in apt) export DEBIAN_FRONTEND=noninteractive if [[ -n "$SUDO" ]]; then sudo -E apt-get update -qq || { fail "apt-get update failed"; return 1; } sudo -E apt-get upgrade -y -qq \ -o Dpkg::Options::="--force-confdef" \ -o Dpkg::Options::="--force-confold" else apt-get update -qq || { fail "apt-get update failed"; return 1; } apt-get upgrade -y -qq \ -o Dpkg::Options::="--force-confdef" \ -o Dpkg::Options::="--force-confold" fi rc=$? ;; dnf) $SUDO dnf upgrade --refresh -y -q ; rc=$? ;; yum) $SUDO yum update -y -q ; rc=$? ;; pacman) $SUDO pacman -Syu --noconfirm ; rc=$? ;; zypper) $SUDO zypper --non-interactive refresh && $SUDO zypper --non-interactive update ; rc=$? ;; apk) $SUDO apk update -q && $SUDO apk upgrade ; rc=$? ;; esac if ((rc == 0)); then ok "OS packages updated (distribution version was NOT changed)." return 0 else fail "OS update encountered errors — review output above." return 1 fi } # ---------- Install Docker ---------- install_docker_debian() { proc "Installing Docker on Debian/Ubuntu..." ensure_dep curl ensure_dep gpg gnupg ensure_dep lsb_release lsb-release || true $SUDO install -m 0755 -d /etc/apt/keyrings local repo_id="$OS_ID" [[ "$OS_ID" != "ubuntu" && "$OS_ID" != "debian" ]] && { case "$OS_LIKE" in *ubuntu*) repo_id="ubuntu";; *) repo_id="debian";; esac } curl -fsSL "https://download.docker.com/linux/${repo_id}/gpg" \ | $SUDO gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg $SUDO chmod a+r /etc/apt/keyrings/docker.gpg local codename="${OS_CODENAME:-$(lsb_release -cs 2>/dev/null || echo stable)}" echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${repo_id} ${codename} stable" \ | $SUDO tee /etc/apt/sources.list.d/docker.list >/dev/null $SUDO apt-get update -qq $SUDO apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin } install_docker_rhel() { proc "Installing Docker on RHEL/Fedora/Rocky/Alma..." ensure_dep curl $SUDO $PKG_MGR install -y -q "${PKG_MGR}-plugins-core" 2>/dev/null || true local repo_id="centos" [[ "$OS_ID" == "fedora" ]] && repo_id="fedora" $SUDO $PKG_MGR config-manager --add-repo "https://download.docker.com/linux/${repo_id}/docker-ce.repo" \ || $SUDO curl -fsSL "https://download.docker.com/linux/${repo_id}/docker-ce.repo" -o /etc/yum.repos.d/docker-ce.repo $SUDO $PKG_MGR install -y -q docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin } install_docker_arch() { proc "Installing Docker on Arch Linux..." $SUDO pacman -Sy --noconfirm --needed docker docker-compose docker-buildx } install_docker_suse() { proc "Installing Docker on openSUSE/SLES..." $SUDO zypper --non-interactive install docker docker-compose } install_docker_alpine() { proc "Installing Docker on Alpine..." $SUDO apk add --quiet docker docker-cli-compose $SUDO rc-update add docker boot 2>/dev/null || true } install_docker() { case "$OS_FAMILY" in debian) install_docker_debian ;; rhel) install_docker_rhel ;; arch) install_docker_arch ;; suse) install_docker_suse ;; alpine) install_docker_alpine ;; *) die "No installer for OS family: $OS_FAMILY" ;; esac proc "Enabling and starting Docker service..." if command -v systemctl >/dev/null 2>&1; then $SUDO systemctl enable --now docker \ || die "Failed to enable/start docker.service" elif command -v rc-service >/dev/null 2>&1; then $SUDO rc-service docker start || die "Failed to start docker" fi proc "Verifying installation..." command -v docker >/dev/null 2>&1 || die "docker binary still missing after install" $SUDO docker info >/dev/null 2>&1 || die "docker daemon not reachable after install" $SUDO docker compose version >/dev/null 2>&1 || die "docker compose plugin not available after install" ok "Docker: $(docker --version)" ok "Docker Compose: $($SUDO docker compose version)" proc "Running hello-world container as final sanity check..." if $SUDO docker run --rm hello-world >/dev/null 2>&1; then ok "hello-world container ran successfully" else fail "hello-world failed — check network/daemon, but binaries are installed" fi if [[ $EUID -ne 0 ]]; then proc "Adding user '$(whoami)' to docker group..." $SUDO usermod -aG docker "$(whoami)" && \ ok "User added to docker group — log out and back in to use docker without sudo" fi } # ---------- Main ---------- main() { banner setup_sudo detect_os set +e check_docker local rc=$? set -e case $rc in 0) ok "Docker and Docker Compose are already installed and working." # Check for available updates check_docker_updates || true if ((${#UPDATE_PKGS[@]} > 0)); then if ask_yn "Download and install these Docker updates now?" Y; then apply_docker_updates else info "Skipping Docker updates per user choice." fi fi ;; 2) info "Docker is installed but the Compose plugin is missing." if ask_yn "Install the missing Docker Compose plugin now?" Y; then case "$OS_FAMILY" in debian) $SUDO apt-get update -qq && $SUDO apt-get install -y -qq docker-compose-plugin ;; rhel) $SUDO $PKG_MGR install -y -q docker-compose-plugin ;; arch) $SUDO pacman -Sy --noconfirm --needed docker-compose ;; suse) $SUDO zypper --non-interactive install docker-compose ;; alpine) $SUDO apk add --quiet docker-cli-compose ;; esac $SUDO docker compose version >/dev/null 2>&1 \ && ok "Compose plugin installed: $($SUDO docker compose version)" \ || die "Compose plugin install failed" else info "Skipping Compose plugin install per user choice." fi # Also offer Docker component updates check_docker_updates || true if ((${#UPDATE_PKGS[@]} > 0)) && ask_yn "Download and install these Docker updates now?" Y; then apply_docker_updates fi ;; 1) info "Docker is NOT installed." if ask_yn "Install Docker CE + Compose plugin from the official repository now?" Y; then install_docker ok "Installation complete." else info "User declined Docker installation. Exiting." exit 0 fi ;; esac # Final OS update prompt (never distro upgrade) echo info "Final step: operating system package updates." info "This will apply security + regular updates but will NEVER upgrade the distribution version." if ask_yn "Run OS package updates now?" N; then os_update else info "Skipping OS updates per user choice." fi echo ok "All done. Enjoy your Docker setup!" exit 0 } main "$@"