添加 install-docker-cn.sh

This commit is contained in:
2025-10-22 10:48:50 +08:00
commit 0bf76f4384

327
install-docker-cn.sh Normal file
View File

@@ -0,0 +1,327 @@
#!/usr/bin/env bash
set -euo pipefail
# 教学用:在中国地区自动安装 Docker Engine
# 功能:
# 1) 识别系统类型 (Debian/Ubuntu, RHEL/CentOS/Alma/Rocky, Fedora)
# 2) 自动挑选国内可用且尽量更快的 Docker 下载镜像
# 3) 通过官方 get.docker.com 脚本安装(并传入镜像环境变量)
# 4) 可选:为 Docker Hub 配置一个国内 registry mirror若 daemon.json 不存在则写入)
# 5) 启动并开机自启,当前用户加入 docker 组
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then SUDO="sudo"; else SUDO=""; fi
# 可选参数:
# --no-registry-mirror 跳过为 Docker Hub 写入 registry-mirrors
# --force-mirror 即使已有 daemon.json 也强制写入/更新 registry-mirrors优先 jq 合并,不在则备份后覆盖)
# --mirror "URL1,URL2" 自定义镜像加速器列表(逗号或空格分隔)
# --max-attempts N 下载镜像尝试次数(默认 5
DISABLE_MIRROR=0
FORCE_MIRROR=0
CUSTOM_MIRRORS=""
MAX_ATTEMPTS=5
while [[ $# -gt 0 ]]; do
case "$1" in
--no-registry-mirror) DISABLE_MIRROR=1; shift ;;
--force-mirror) FORCE_MIRROR=1; shift ;;
--mirror) shift; CUSTOM_MIRRORS="${1:-}"; shift ;;
--max-attempts) shift; MAX_ATTEMPTS=${1:-5}; shift ;;
-h|--help)
cat <<USAGE
用法: $0 [选项]
--no-registry-mirror 跳过为 Docker Hub 写入 registry-mirrors
--force-mirror 强制写入/更新 registry-mirrors有 jq 则合并,无 jq 则备份后覆盖)
--mirror "URL1,URL2" 指定一个或多个加速器,逗号/空格分隔(默认自动测速选择)
--max-attempts N 下载镜像尝试次数(默认 5
-h, --help 显示帮助
USAGE
exit 0
;;
*) warn "忽略未知参数:$1"; shift ;;
esac
done
log() { printf "[INFO] %s\n" "$*"; }
warn() { printf "[WARN] %s\n" "$*" 1>&2; }
err() { printf "[ERR ] %s\n" "$*" 1>&2; }
need_cmd() { command -v "$1" >/dev/null 2>&1; }
install_prereqs() {
if need_cmd curl && need_cmd awk; then return 0; fi
if command -v apt-get >/dev/null 2>&1; then
$SUDO apt-get update -y || true
$SUDO apt-get install -y curl ca-certificates gnupg lsb-release awk || true
elif command -v dnf >/dev/null 2>&1; then
$SUDO dnf install -y curl ca-certificates gawk || true
elif command -v yum >/dev/null 2>&1; then
$SUDO yum install -y curl ca-certificates gawk || true
fi
if ! need_cmd curl; then err "缺少 curl请手动安装后重试"; exit 1; fi
if ! need_cmd awk; then err "缺少 awk请手动安装后重试"; exit 1; fi
}
detect_family() {
local id like
id=""; like=""
if [[ -r /etc/os-release ]]; then
. /etc/os-release
id=${ID:-}
like=${ID_LIKE:-}
fi
case "$id" in
ubuntu|debian|raspbian) echo debian; return;;
fedora) echo fedora; return;;
centos|rhel|rocky|almalinux|ol|anolis) echo rhel; return;;
esac
if [[ "$like" =~ (debian) ]]; then echo debian; return; fi
if [[ "$like" =~ (rhel|centos|fedora) ]]; then echo rhel; return; fi
echo unknown
}
pick_fastest_download_base() {
# 返回Name|DOWNLOAD_URL REPO_URL = ${DOWNLOAD_URL}/linux
# 支持通过环境变量 EXCLUDE_URLS用空格分隔来排除已失败的地址
local -a candidates=(
"Aliyun|https://mirrors.aliyun.com/docker-ce"
"Tsinghua|https://mirrors.tuna.tsinghua.edu.cn/docker-ce"
"USTC|https://mirrors.ustc.edu.cn/docker-ce"
"Huawei|https://repo.huaweicloud.com/docker-ce"
"Official|https://download.docker.com"
)
local best_name="" best_url="" best_time="999999"
local item name url code time http_and_time skip
for item in "${candidates[@]}"; do
name="${item%%|*}"; url="${item#*|}"
skip=0
for ex in ${EXCLUDE_URLS:-}; do [[ "$url" == "$ex" ]] && skip=1 && break; done
[[ $skip -eq 1 ]] && continue
http_and_time=$(curl -m 4 -L -s -o /dev/null -w "%{http_code} %{time_total}" "${url}/linux/") || true
code="${http_and_time%% *}"; time="${http_and_time#* }"
[[ -z "$time" ]] && time="999999"
if [[ "$code" =~ ^2 ]] || [[ "$code" =~ ^3 ]]; then
log "镜像可用:$name $url (${time}s)"
if awk -v t="$time" -v b="$best_time" 'BEGIN{exit (t < b) ? 0 : 1}'; then
best_time="$time"; best_url="$url"; best_name="$name"
fi
else
warn "镜像不可用:$name $url (HTTP $code)"
fi
done
if [[ -z "$best_url" ]]; then
warn "未探测到可用镜像使用官方https://download.docker.com"
best_name="Official"; best_url="https://download.docker.com"
fi
printf "%s|%s\n" "$best_name" "$best_url"
}
pick_fastest_registry_mirror() {
# 返回Name|URL ;挑选 Docker Hub 加速器
local -a mirrors=(
"Tencent |https://mirror.ccs.tencentyun.com"
"USTC |https://docker.mirrors.ustc.edu.cn"
"1PanelP |https://dockerproxy.1panel.live"
"1Panel |https://docker.1panel.live"
"1PanelX |https://docker.1panelproxy.com"
"1Proxy |https://proxy.1panel.live"
"NetEase |https://hub-mirror.c.163.com"
"Proxy |https://dockerproxy.com"
)
local best_name="" best_url="" best_time="999999"
local item name url code time http_and_time
for item in "${mirrors[@]}"; do
name="${item%%|*}"; url="${item#*|}"
http_and_time=$(curl -m 4 -L -s -o /dev/null -w "%{http_code} %{time_total}" "${url}/v2/") || true
code="${http_and_time%% *}"; time="${http_and_time#* }"
[[ -z "$time" ]] && time="999999"
if [[ "$code" =~ ^2 ]] || [[ "$code" =~ ^3 ]] || [[ "$code" =~ ^4 ]]; then
log "Hub 加速器可用:$name $url (${time}s)"
if awk -v t="$time" -v b="$best_time" 'BEGIN{exit (t < b) ? 0 : 1}'; then
best_time="$time"; best_url="$url"; best_name="$name"
fi
else
warn "Hub 加速器不可用:$name $url (HTTP $code)"
fi
done
if [[ -z "$best_url" ]]; then
warn "未探测到可用 Hub 加速器,将不写入 registry-mirrors"
return 1
fi
printf "%s|%s\n" "$best_name" "$best_url"
}
# 返回按速度排序的可用加速器(空格分隔)
pick_registry_mirrors_sorted() {
local -a urls=(
"https://mirror.ccs.tencentyun.com"
"https://docker.mirrors.ustc.edu.cn"
"https://dockerproxy.1panel.live"
"https://docker.1panel.live"
"https://docker.1panelproxy.com"
"https://proxy.1panel.live"
"https://hub-mirror.c.163.com"
"https://dockerproxy.com"
)
local lines="" u http_and_time code time
for u in "${urls[@]}"; do
http_and_time=$(curl -m 4 -L -s -o /dev/null -w "%{http_code} %{time_total}" "$u/v2/") || true
code="${http_and_time%% *}"; time="${http_and_time#* }"
[[ -z "$time" ]] && time="999999"
if [[ "$code" =~ ^2 ]] || [[ "$code" =~ ^3 ]] || [[ "$code" =~ ^4 ]]; then
log "Hub 加速器可用:$u (${time}s)"
lines+="$time $u\n"
else
warn "Hub 加速器不可用:$u (HTTP $code)"
fi
done
if [[ -z "$lines" ]]; then return 1; fi
printf "%s" "$lines" | LC_ALL=C sort -n | awk '{print $2}' | tr '\n' ' '
}
mirrors_to_json_array() {
# 输入:以空格分隔的镜像 URL 列表输出JSON 数组字符串
# 例:"https://a https://b" -> ["https://a","https://b"]
local arr=($1)
local out="["
local i
for ((i=0;i<${#arr[@]};i++)); do
out+="\"${arr[$i]}\""
if (( i < ${#arr[@]}-1 )); then out+=","; fi
done
out+="]"
printf "%s" "$out"
}
ensure_service_started() {
if command -v systemctl >/dev/null 2>&1; then
$SUDO systemctl daemon-reload || true
$SUDO systemctl enable --now docker || $SUDO systemctl start docker || true
elif command -v service >/dev/null 2>&1; then
$SUDO service docker start || true
else
warn "未检测到 systemd/service尝试直接后台启动 dockerd"
if need_cmd dockerd; then nohup $SUDO dockerd >/var/log/dockerd.log 2>&1 & fi
fi
}
post_install() {
local target_user
if [[ -n "${SUDO_USER:-}" && "${SUDO_USER:-}" != "root" ]]; then
target_user="$SUDO_USER"
else
target_user="$(id -un)"
fi
$SUDO groupadd -f docker || true
if [[ "$target_user" != "root" ]]; then
$SUDO usermod -aG docker "$target_user" || true
log "已将用户 $target_user 加入 docker 组(重新登录后生效)"
fi
}
main() {
install_prereqs
local family
family=$(detect_family)
if [[ "$family" == "unknown" ]]; then
warn "未识别的系统,仍尝试安装(若失败请改为手动仓库方式)"
else
log "检测到系统族:$family"
fi
local chosen name base_url
local tmp_script
tmp_script=$(mktemp /tmp/get-docker.XXXXXX.sh)
log "下载官方安装脚本 get.docker.com ..."
if ! curl -fsSL -m 30 https://get.docker.com -o "$tmp_script"; then
err "下载 get.docker.com 失败,请检查网络或稍后再试"
exit 2
fi
# 循环尝试:每次挑选当前最快镜像,失败则加入排除列表并继续
local attempt=1 success=0
EXCLUDE_URLS=""
while [[ $attempt -le $MAX_ATTEMPTS ]]; do
chosen=$(pick_fastest_download_base)
name="${chosen%%|*}"; base_url="${chosen#*|}"
log "${attempt}/${MAX_ATTEMPTS} 次安装尝试:$name -> $base_url"
if DOWNLOAD_URL="$base_url" REPO_URL="$base_url/linux" sh "$tmp_script"; then
success=1
break
else
warn "使用镜像 $base_url 安装失败,切换下一个镜像重试"
EXCLUDE_URLS="$EXCLUDE_URLS $base_url"
attempt=$((attempt+1))
fi
done
if [[ $success -ne 1 ]]; then
err "所有镜像尝试均失败,请检查网络或改用手动仓库方法"
exit 3
fi
# 配置 registry mirror支持自定义/强制覆盖/跳过)
if [[ $DISABLE_MIRROR -eq 0 ]]; then
local mirror_list=""
if [[ -n "$CUSTOM_MIRRORS" ]]; then
mirror_list="$(echo "$CUSTOM_MIRRORS" | tr ',' ' ')"
log "使用自定义加速器:$mirror_list"
else
if mirror_list=$(pick_registry_mirrors_sorted); then
log "写入测速排序后的加速器列表:$mirror_list"
else
warn "未找到可用加速器,跳过写入 registry-mirrors"
fi
fi
if [[ -n "$mirror_list" ]]; then
local json_arr
json_arr=$(mirrors_to_json_array "$mirror_list")
$SUDO install -d -m 0755 /etc/docker
if [[ -f /etc/docker/daemon.json ]]; then
if command -v jq >/dev/null 2>&1; then
local tmpf
tmpf=$(mktemp)
if $SUDO jq --argjson arr "$json_arr" '."registry-mirrors"=$arr' /etc/docker/daemon.json > "$tmpf" 2>/dev/null; then
$SUDO mv "$tmpf" /etc/docker/daemon.json
log "已用 jq 更新 /etc/docker/daemon.json 的 registry-mirrors"
else
warn "jq 更新失败,跳过更新(可使用 --force-mirror 覆盖)"
rm -f "$tmpf" 2>/dev/null || true
fi
elif [[ $FORCE_MIRROR -eq 1 ]]; then
local bak="/etc/docker/daemon.json.bak-$(date +%Y%m%d%H%M%S)"
$SUDO cp /etc/docker/daemon.json "$bak"
warn "未检测到 jq已备份旧配置到 $bak,并覆盖写入 registry-mirrors"
$SUDO sh -c "cat > /etc/docker/daemon.json" <<EOF
{
"registry-mirrors": $json_arr
}
EOF
else
warn "/etc/docker/daemon.json 已存在,为避免破坏现有配置未写入。可用 --force-mirror 或预装 jq 后重试。"
fi
else
$SUDO tee /etc/docker/daemon.json >/dev/null <<EOF
{
"registry-mirrors": $json_arr
}
EOF
fi
fi
else
log "按参数要求跳过 registry-mirrors 配置"
fi
ensure_service_started
post_install
log "安装完成!建议执行:"
printf " docker version\n"
printf " docker run --rm hello-world\n"
}
main "$@"