#!/usr/bin/env bash # ============================================================================= # 00-init-tools.sh — 安装并配置服务器基础工具环境 # # 功能: # 1. 安装 Go(默认 1.22.5,可通过参数指定版本) # 2. 配置 GOPROXY(自动测速选最快节点) # 3. 安装 Node.js / npm(前端依赖) # 4. 安装 Docker(基础设施容器) # 5. 安装 etcdctl(查看 Etcd 服务注册) # 6. 安装 Nginx 并写入 PC/CMS/Build-CMS/Build-Down/OpenIM 反代(本机 HTTP :80;外部 HTTPS 由 LB/CDN 终止) # 7. 写入 /etc/profile.d/deploy-env.sh(永久生效) # # 用法: # ./deploy-test/00-init-tools.sh # 安装全部(含 Nginx 反代) # ./deploy-test/00-init-tools.sh go # 只安装/配置 Go # ./deploy-test/00-init-tools.sh node # 只安装 Node.js # ./deploy-test/00-init-tools.sh docker # 只安装 Docker # ./deploy-test/00-init-tools.sh goproxy # 只配置 GOPROXY # sudo ./deploy-test/00-init-tools.sh etcdctl # 只安装 etcdctl(需 root) # sudo ./deploy-test/00-init-tools.sh nginx # 只安装 Nginx 反代(需 root) # # 前置条件: root 或 sudo 权限,Ubuntu/Debian 系统 # # 下一步: ./deploy-test/01-init-env.sh # ============================================================================= set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" init_dirs init_script_log # ── 可调参数 ─────────────────────────────────────────────────────────────────── GO_VERSION="${GO_VERSION:-1.22.5}" GO_ARCH="${GO_ARCH:-amd64}" # amd64 / arm64 NODE_VERSION="${NODE_VERSION:-20}" # Node.js LTS 大版本 ETCDCTL_VERSION="${ETCDCTL_VERSION:-3.5.17}" PROFILE_FILE="/etc/profile.d/deploy-env.sh" TARGET="${1:-all}" header "步骤 0 — 初始化工具环境" echo " 目标: ${TARGET} (Go=${GO_VERSION}, Node=${NODE_VERSION})" # ────────────────────────────────────────────────────────────────────────────── # 辅助函数 # ────────────────────────────────────────────────────────────────────────────── # 检查命令是否存在(不触发 set -e) _has() { command -v "$1" &>/dev/null; } # 追加到全局 profile(幂等:同一行不重复写) _append_profile() { local line="$1" grep -qxF "$line" "$PROFILE_FILE" 2>/dev/null || echo "$line" >> "$PROFILE_FILE" } # 测试 GOPROXY 节点延迟,返回 ms(超时返回 9999) _probe_proxy() { local url="${1%/}/github.com/gin-gonic/gin/@v/list" local ms ms=$(curl -s -o /dev/null -w "%{time_total}" --max-time 5 "$url" 2>/dev/null || echo "9.999") echo "${ms/./}" | sed 's/^0*//' | awk '{printf "%d\n", $1 * 1000 / 1000 + 0}' # 简单地把秒转毫秒并取整 python3 -c "print(int(float('${ms}') * 1000))" 2>/dev/null || echo 9999 } # ────────────────────────────────────────────────────────────────────────────── # 1. Go # ────────────────────────────────────────────────────────────────────────────── _install_go() { step "安装 Go ${GO_VERSION} (linux/${GO_ARCH})" local tarball="go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" local url="https://go.dev/dl/${tarball}" local tmp="/tmp/${tarball}" if _has go; then local cur cur=$(go version | awk '{print $3}' | sed 's/go//') if [[ "$cur" == "$GO_VERSION" ]]; then success " Go ${GO_VERSION} 已安装,跳过" return 0 fi warn " 当前 Go 版本 ${cur},将升级到 ${GO_VERSION}" fi info " 下载 ${url}" curl -fL --progress-bar -o "$tmp" "$url" || { # 备用镜像 warn " go.dev 下载失败,尝试备用镜像..." curl -fL --progress-bar -o "$tmp" "https://golang.google.cn/dl/${tarball}" || { error " 下载失败,请手动安装: https://go.dev/dl/" return 1 } } info " 解压到 /usr/local/go" rm -rf /usr/local/go tar -C /usr/local -xzf "$tmp" rm -f "$tmp" # 写入 profile [[ -f "$PROFILE_FILE" ]] || touch "$PROFILE_FILE" _append_profile 'export PATH=$PATH:/usr/local/go/bin' _append_profile 'export GOPATH=/root/go' _append_profile 'export GOMODCACHE=/root/go/pkg/mod' # 当前会话也生效 export PATH=$PATH:/usr/local/go/bin export GOPATH=/root/go export GOMODCACHE=/root/go/pkg/mod success " Go $(go version | awk '{print $3}') 安装完成 → /usr/local/go/bin/go" } # ────────────────────────────────────────────────────────────────────────────── # 2. GOPROXY — 自动选最快节点 # ────────────────────────────────────────────────────────────────────────────── _config_goproxy() { step "配置 GOPROXY(自动测速)" if ! _has go; then warn " go 未安装,跳过 GOPROXY 配置" return 0 fi declare -A PROXIES=( ["proxy.golang.org"]="https://proxy.golang.org" ["goproxy.cn"]="https://goproxy.cn" ["goproxy.io"]="https://goproxy.io" ) local best_name="goproxy.io" local best_ms=9999 info " 测速中..." for name in "${!PROXIES[@]}"; do local url="${PROXIES[$name]}" local ms ms=$(python3 -c " import urllib.request, time, sys try: t = time.time() urllib.request.urlopen('${url}/github.com/gin-gonic/gin/@v/list', timeout=5) print(int((time.time()-t)*1000)) except: print(9999) " 2>/dev/null) echo " ${name}: ${ms}ms" if (( ms < best_ms )); then best_ms=$ms best_name=$name best_url="${url}" fi done # 构建代理列表:最快的放第一 local proxy_list="${best_url},https://proxy.golang.org,https://goproxy.cn,direct" # 去重 proxy_list=$(echo "$proxy_list" | tr ',' '\n' | awk '!seen[$0]++' | tr '\n' ',' | sed 's/,$//') go env -w GOPROXY="${proxy_list}" go env -w GONOSUMDB="*" go env -w GOFLAGS="" # 同步写入 profile(go env -w 已持久化到 GOENV,这里写 profile 作为双保险) _append_profile "export GOPROXY=${proxy_list}" _append_profile 'export GONOSUMDB=*' success " GOPROXY=${proxy_list}" success " 最快节点: ${best_name} (${best_ms}ms)" } # ────────────────────────────────────────────────────────────────────────────── # 3. Node.js # ────────────────────────────────────────────────────────────────────────────── _install_node() { step "安装 Node.js ${NODE_VERSION}.x LTS" if _has node; then local cur cur=$(node --version) info " 当前 Node.js 版本: ${cur}" # 检查大版本是否匹配 local major major=$(echo "$cur" | sed 's/v//' | cut -d. -f1) if (( major >= NODE_VERSION )); then success " Node.js >= ${NODE_VERSION},跳过" return 0 fi warn " 版本过旧,将升级" fi if _has apt-get; then info " 使用 NodeSource 安装 Node.js ${NODE_VERSION}.x" curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash - apt-get install -y nodejs elif _has yum; then curl -fsSL "https://rpm.nodesource.com/setup_${NODE_VERSION}.x" | bash - yum install -y nodejs else warn " 未找到 apt-get/yum,请手动安装 Node.js: https://nodejs.org/" return 1 fi # 安装常用全局包管理器 npm install -g pnpm yarn 2>/dev/null || true _append_profile 'export PATH=$PATH:/usr/local/bin' # 将 GitHub SSH 地址重写为 HTTPS(服务器通常没有 GitHub SSH 密钥) git config --global url."https://github.com/".insteadOf "git+ssh://git@github.com/" 2>/dev/null || true git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" 2>/dev/null || true git config --global url."https://github.com/".insteadOf "git@github.com:" 2>/dev/null || true success " Node.js $(node --version),npm $(npm --version) 安装完成" _has pnpm && success " pnpm $(pnpm --version)" _has yarn && success " yarn $(yarn --version)" } # ────────────────────────────────────────────────────────────────────────────── # 4. Docker # ────────────────────────────────────────────────────────────────────────────── _install_docker() { step "安装 Docker" if _has docker; then success " Docker $(docker --version | awk '{print $3}' | tr -d ',') 已安装,跳过" return 0 fi if _has apt-get; then info " 使用官方脚本安装 Docker..." curl -fsSL https://get.docker.com | sh elif _has yum; then yum install -y docker systemctl enable docker else warn " 未找到 apt-get/yum,请手动安装 Docker: https://docs.docker.com/engine/install/" return 1 fi # 启动服务 systemctl start docker 2>/dev/null || service docker start 2>/dev/null || true success " Docker $(docker --version | awk '{print $3}' | tr -d ',') 安装完成" } # ────────────────────────────────────────────────────────────────────────────── # 5. etcdctl # ────────────────────────────────────────────────────────────────────────────── _install_etcdctl() { step "安装 etcdctl ${ETCDCTL_VERSION}" if _has etcdctl; then success " etcdctl 已安装: $(etcdctl version | head -1)" return 0 fi if [[ "$(id -u)" -ne 0 ]]; then warn " etcdctl 安装需 root,请执行: sudo $0 etcdctl" return 0 fi if _has apt-get; then apt-get update -y if apt-get install -y etcd-client; then success " etcdctl 安装完成: $(etcdctl version | head -1)" return 0 fi warn " apt 安装 etcd-client 失败,尝试下载官方二进制" elif _has dnf; then if dnf install -y etcd; then success " etcdctl 安装完成: $(etcdctl version | head -1)" return 0 fi warn " dnf 安装 etcd 失败,尝试下载官方二进制" elif _has yum; then if yum install -y etcd; then success " etcdctl 安装完成: $(etcdctl version | head -1)" return 0 fi warn " yum 安装 etcd 失败,尝试下载官方二进制" fi local arch case "$(uname -m)" in x86_64|amd64) arch="amd64" ;; aarch64|arm64) arch="arm64" ;; *) error " 不支持的架构: $(uname -m),请手动安装 etcdctl" return 1 ;; esac local tarball="etcd-v${ETCDCTL_VERSION}-linux-${arch}.tar.gz" local url="https://github.com/etcd-io/etcd/releases/download/v${ETCDCTL_VERSION}/${tarball}" local tmp="/tmp/${tarball}" local out_dir="/tmp/etcd-v${ETCDCTL_VERSION}-linux-${arch}" info " 下载 ${url}" curl -fL --progress-bar -o "$tmp" "$url" rm -rf "$out_dir" tar -C /tmp -xzf "$tmp" install -m 0755 "${out_dir}/etcdctl" /usr/local/bin/etcdctl rm -rf "$tmp" "$out_dir" success " etcdctl 安装完成: $(etcdctl version | head -1)" } # ────────────────────────────────────────────────────────────────────────────── # 6. Nginx — PC / CMS / Build-CMS / Build-Down / OpenIM 统一入口(本机 HTTP :80) # ────────────────────────────────────────────────────────────────────────────── _install_pc_nginx_proxy() { step "安装 Nginx 并配置 OpenIM/PC/CMS/Build-CMS/Build-Down 反代" if [[ "$(id -u)" -ne 0 ]]; then error " Nginx 安装需 root,请执行: sudo $0 nginx" return 1 fi local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local conf_src="${script_dir}/nginx/openim-pc-proxy.conf" local conf_name="openim-pc-proxy.conf" local proxy_domain="${PC_PROXY_DOMAIN:-pc-jack.imharry.work}" if [[ -f "$ENV_FILE" ]]; then # shellcheck source=/dev/null source "$ENV_FILE" proxy_domain="${PC_PROXY_DOMAIN:-$proxy_domain}" fi if [[ ! -f "$conf_src" ]]; then error " 找不到配置: $conf_src" return 1 fi if ! _has nginx; then if _has apt-get; then apt-get update -y apt-get install -y nginx elif _has dnf; then dnf install -y nginx elif _has yum; then yum install -y nginx else error " 未检测到 apt/dnf/yum,请先手动安装 nginx" return 1 fi fi if [[ -d /etc/nginx/sites-available ]]; then install -m 0644 "$conf_src" "/etc/nginx/sites-available/${conf_name}" mkdir -p /etc/nginx/sites-enabled ln -sf "/etc/nginx/sites-available/${conf_name}" "/etc/nginx/sites-enabled/${conf_name}" if [[ -f /etc/nginx/sites-enabled/default ]]; then rm -f /etc/nginx/sites-enabled/default info " 已移除 sites-enabled/default,避免与 openim-pc-proxy 冲突" fi # 保留 sites-available/default 文件时,去掉 default_server,防止日后误启用再次抢占 :80 if [[ -f /etc/nginx/sites-available/default ]]; then sed -i.bak-openim \ -e 's/listen \[::\]:80 default_server;/listen [::]:80;/g' \ -e 's/listen 80 default_server;/listen 80;/g' \ /etc/nginx/sites-available/default 2>/dev/null || true info " 已去除 sites-available/default 中的 default_server(若存在)" fi else install -m 0644 "$conf_src" "/etc/nginx/conf.d/${conf_name}" fi nginx -t systemctl enable nginx 2>/dev/null || true systemctl restart nginx local health_failed=0 local host for host in \ "$proxy_domain" \ "cms-jack.imharry.work" \ "build-jack.imharry.work" \ "down-jack.imharry.work" do if curl -fsS --max-time 3 -H "Host: ${host}" http://127.0.0.1/nginx-health >/dev/null 2>&1; then success " Host 路由已生效: ${host}" else error " Host 路由检查失败: ${host}" health_failed=1 fi done success " Nginx 反代已启用(配置: $conf_src)" info " 本机 Nginx 仅监听 TCP 80;curl -sS -H 'Host: ${proxy_domain}' http://127.0.0.1/nginx-health 应返回 ok" info " 外部 HTTPS 可由 LB/CDN/其它网关终止后转发到本机 :80" info " PC 入口: https://${proxy_domain}/" info " CMS 入口: http://cms-jack.imharry.work/" info " Build CMS 入口: http://build-jack.imharry.work/" info " Build Down 入口: http://down-jack.imharry.work/" if [[ "$health_failed" -ne 0 ]]; then error " Nginx 已重启,但部分 Host 路由校验失败,请检查 server_name / 默认站点 / 其它占用 :80 的配置" return 1 fi } # all 时非 root 则跳过(不中断 Go/Node/Docker/etcdctl) _run_nginx_if_root() { if [[ "$(id -u)" -eq 0 ]]; then _install_pc_nginx_proxy else warn " 当前非 root,已跳过 Nginx。需要时在服务器上执行: sudo $0 nginx" fi } # ────────────────────────────────────────────────────────────────────────────── # 执行 # ────────────────────────────────────────────────────────────────────────────── case "$TARGET" in go) _install_go _config_goproxy ;; goproxy) _config_goproxy ;; node) _install_node ;; docker) _install_docker ;; etcdctl) _install_etcdctl ;; nginx) _install_pc_nginx_proxy ;; all) _install_go _config_goproxy _install_node _install_docker _install_etcdctl _run_nginx_if_root ;; *) error "未知目标: $TARGET" echo "用法: $0 [all|go|goproxy|node|docker|etcdctl|nginx]" exit 1 ;; esac # ────────────────────────────────────────────────────────────────────────────── # 汇总 # ────────────────────────────────────────────────────────────────────────────── echo "" success "环境初始化完成!" echo "" echo -e "${BOLD}当前工具版本:${NC}" _has go && echo " Go: $(go version | awk '{print $3, $4}')" || echo " Go: 未安装" _has node && echo " Node: $(node --version)" || echo " Node: 未安装" _has npm && echo " npm: $(npm --version)" || echo " npm: 未安装" _has pnpm && echo " pnpm: $(pnpm --version)" || echo " pnpm: 未安装" _has yarn && echo " yarn: $(yarn --version)" || echo " yarn: 未安装" _has docker && echo " Docker: $(docker --version | awk '{print $3}' | tr -d ',')" || echo " Docker: 未安装" _has etcdctl && echo " etcdctl: $(etcdctl version | head -1 | sed 's/^etcdctl version: //')" || echo " etcdctl: 未安装" _has nginx && echo " Nginx: $(nginx -v 2>&1 | sed 's/^nginx version: //')" || echo " Nginx: 未安装" echo "" echo -e "${BOLD}GOPROXY 配置:${NC}" _has go && go env GOPROXY || echo " (go 未安装)" echo "" echo -e "${YELLOW}注意: 新终端需执行以下命令使环境变量生效:${NC}" echo -e " ${CYAN}source /etc/profile.d/deploy-env.sh${NC}" echo -e " 或重新登录 SSH" echo "" echo -e "${BOLD}下一步:${NC}" echo -e " ${CYAN}./deploy-test/01-init-env.sh${NC}"