首页 / 资讯中心 / 文章详情

Shell脚本实战:Docker容器启停、镜像构建与批量部署自动化指南

Shell脚本实战:Docker容器启停、镜像构建与批量部署自动化指南 ★ FEATURED ARTICLE
先说一个观点Docker 的命令本身不难难的是把一堆命令按照正确的顺序、在正确的时机、带着正确的参数执行出来。我最早接触容器时发布一次服务要 SSH 上去敲七八条命令敲完之后还要盯着日志确认状态碰到多台机器更是折磨。后来我把这些操作全部改写成 Shell 脚本容器启停、镜像构建、批量部署这三件事从“手工重复劳动”变成了“一条命令跑完”。这篇文章就把我这套 Shell 结合 Docker 的自动化实践完整拆开从脚本设计思路、核心代码逻辑到真实踩过的坑一次讲清楚。适合刚学 Docker 想规范操作的研发也适合手上管着若干台服务器、想从手工发布解放出来的运维同学。1. 为什么非要用 Shell 写 Docker 自动化——聊聊手动操作的三个死穴1.1 手动敲命令的坑不是慢而是不可复现很多人觉得“不就是几条 docker 命令吗我敲得很快”。快是快但问题在于手动操作充满了隐性差异这次发布用的是docker run -d -p 8080:8080下次可能忘了-e环境变量这次构建用的标签是v1.2.0下次随手写了个latest等回滚的时候根本不知道该回滚到哪个镜像。Shell 脚本天然就是干这个的它能把命令、参数、顺序全部固化下来让每一次发布都走完全相同的路径。我举个例子手动发布一次典型的 Web 服务大概要做这几步拉代码、切分支、构建镜像、停旧容器、删旧容器、启动新容器、清理悬空镜像。七步操作里面任何一步敲错都可能制造一个“僵尸容器”或者镜像名错乱。脚本化之后同样的流程变成./deploy.sh product-web内部逻辑完全一致出差错的空间被压缩到最小。1.2 Shell 和 Docker CLI 的契合点一切都是文本流Shell 之所以适合编排 Docker本质原因是 Docker CLI 设计得非常“Unix 友好”——它的输出是标准文本支持各种命令行参数组合退出码也足够准确。这意味着你可以用非常朴素的方式组合它们用变量保存容器名用循环遍历容器列表用条件判断检查执行结果用管道对输出做二次处理。这正好把 Shell 的优势发挥到极致。另外Shell 脚本不需要额外安装任何东西。每台 Linux 服务器上都有 bash而你只要装了 Docker那必然有docker命令。相比起上 Python 或 Go 写运维工具Shell 脚本是零依赖、零编译、随处可跑的方案尤其在批量处理多台机器时——把脚本 scp 过去就能执行不用考虑运行库、依赖包、版本兼容这是它最大的价值。1.3 这套自动化脚本适合哪些场景先泼一盆冷水如果你只有一台机器、两三个容器那老老实实手敲命令或者直接用 Docker Compose 就够用上脚本反而多余。Shell 编排的价值在下面这些场景里才能真正体现多容器的启停顺序要求。比如先启数据库、再启缓存、最后启业务服务一套脚本可以保证顺序不依赖人记忆。镜像构建需要规范化管理。标签如何生成、构建缓存如何处理、多平台要不要支持代码写死比每次头脑临时决策可靠得多。批量部署到多台服务器。一台台 SSH 上去敲同样的命令纯粹是浪费生命。脚本配一个服务器列表循环执行就能把活干完。定时任务和无人值守的运维动作。比如每天凌晨清理悬空镜像、每周重建某些容器这些都是可以完全脚本化、丢给 crontab 去跑的。所以我的判断是Shell 结合 Docker 适合作为“个人/小团队的运维基础设施”它不重、不复杂但能覆盖 80% 的日常容器管理需求。2. 容器启停脚本设计从单容器命令到可复用工具2.1 一个能处理单个容器的启停脚本要具备什么如果你只是写docker stop web docker start web那没啥可讲的。真正的工程化脚本至少要解决三件事处置动作的统一入口、等待容器真正就绪、失败时的退出码处理。我最终的脚本形态是这样的通过一个命令处理 start / stop / restart / status 四种操作#!/bin/bash # 用法: ./container-ctl.sh start|stop|restart|status container_name set -euo pipefail ACTION${1:?缺少操作参数: start|stop|restart|status} CONTAINER${2:?缺少容器名} case $ACTION in start) if docker ps --format {{.Names}} | grep -qx $CONTAINER; then echo [INFO] 容器 $CONTAINER 已在运行跳过启动 else docker start $CONTAINER fi ;; stop) docker stop -t 15 $CONTAINER ;; restart) docker restart -t 15 $CONTAINER ;; status) docker inspect --format 容器: {{.Name}} 状态: {{.State.Status}} PID: {{.State.Pid}} $CONTAINER ;; *) echo 未知操作: $ACTION exit 1 ;; esac这是我在生产环境里实际使用的精简版本。几个设计点说明一下set -euo pipefail是脚本的“安全带”。-e表示任何命令失败就退出脚本-u表示变量未定义即报错pipefail表示管道中任何一环失败都算整体失败。后面踩坑部分我会细讲它的价值。stop -t 15是个容易被忽略的参数。Docker 默认在docker stop时会先发 SIGTERM等 10 秒超时再发 SIGKILL。但很多应用优雅退出的逻辑需要 30 秒以上我习惯显式给-t指定宽限时间。15 秒是我这边 Spring Boot 应用的常见标准你可以根据自己的服务实测值去调整。start 前的docker ps检查是防止重复启动报错。注意这里用管道把它变成一个布尔判断——容器已经在跑就直接跳过这样脚本无论重复执行多少次都是安全的这叫幂等。2.2 用 shift 实现多容器批量操作给脚本加上参数解析热词里有“shell 的 shift 命令”很多新手不太理解 shift 的用途我这里用实际的场景把它讲透。上面的脚本一次只能处理一个容器。但实际场景往往是“我要把这组服务全部重启”比如前端网关、后端服务、定时任务三个容器一起重启。引入shift的思路是这样的把第一个参数作为操作类型用shift把它“踢出参数列表”剩下的参数全部视为容器名然后循环处理#!/bin/bash # 用法: ./multi-ctl.sh restart web cache db set -euo pipefail ACTION${1:?缺少操作参数} shift if [ $# -eq 0 ]; then echo 至少需要指定一个容器名 exit 1 fi for CONTAINER in $; do echo 正在处理容器 $CONTAINER case $ACTION in start) docker start $CONTAINER || echo [WARN] $CONTAINER 启动失败 ;; stop) docker stop -t 15 $CONTAINER || echo [WARN] $CONTAINER 停止失败 ;; restart) docker restart -t 15 $CONTAINER || echo [WARN] $CONTAINER 重启失败 ;; status) docker inspect --format {{.Name}} {{.State.Status}} $CONTAINER || echo [WARN] $CONTAINER 不存在 ;; esac doneshift的作用说白了就是“吃掉第一个参数”。原本$1是 restartshift之后原来的$2变成$1原来的$3变成$2。这样一来$就自动只包含容器名了。写脚本时很多人会绕不开这个点但一旦理解了这其实就是最朴素的参数解析方式——不需要 getopts不需要复杂的逻辑几行代码就把“第一个参数是操作类型其余参数是操作对象”这个最常见的 CLI 模式实现了。这里还有一个细节for CONTAINER in $必须加双引号。如果在for CONTAINER in $不加引号容器名一旦带有空格就会裂成多个词被当成两个容器处理。虽然 Docker 容器名理论上不允许空格但养成加引号的习惯能规避这一类由分词带来的意外。2.3 等待容器“真正可用”而不是“进程还活着”容器启动脚本有一个经典问题docker start返回成功只代表容器起来了不代表容器里的应用已经能对外提供服务。尤其像 Web 服务启动要初始化连接池、加载配置、预热缓存可能需要十几秒。如果脚本立刻去探测端口会发现连接被拒绝。我的做法是在 restart 后加一个健康检查等待函数用循环探测端口wait_for_port() { local host$1 local port$2 local timeout${3:-60} local waited0 while ! timeout 1 bash -c echo /dev/tcp/$host/$port 2/dev/null; do waited$((waited 1)) if [ $waited -ge $timeout ]; then echo [ERROR] 端口 $host:$port 在 ${timeout} 秒内未就绪 return 1 fi echo [INFO] 等待 $host:$port 就绪... ${waited}s sleep 1 done echo [INFO] $host:$port 已就绪耗时 ${waited}s }这段脚本的核心是用 Bash 内建能力猜/dev/tcp/$host/$port进行 TCP 探测。这个技巧比安装nc或curl更省依赖因为它是 Bash 自带的。超过超时时间还没连通函数返回非 0配合set -e整个脚本就会停下来报错而不是“假装部署成功”。在实际项目中我把这个探测动作用在restart动作之后确保网关容器重启完成后再继续重启后面的依赖服务。这种依赖感知顺序是 Shell 编排相对 Compose 更灵活的地方之一。3. 镜像构建自动化把 Dockerfile 以外的脏活累活全包了3.1 构建镜像为什么也要脚本化有人会问构建镜像不就是docker build -t demo .一条命令吗对单独构建一次确实是。但真实生产里构建动作伴随的是一堆周边问题镜像的版本标签怎么生成用日期用 git commit还是手工输入多套环境dev、test、prod是不是要打不同的 tag构建缓存堆积怎么清理上次构建产生的none悬空镜像谁来收拾多架构amd64 / arm64的支持要不要加构建过程中某个步骤依赖的环境变量怎么注入这些问题单个看都不大但累积起来如果每次都在命令行里靠记忆去输入出错的概率会一直存在。我把这些逻辑都写进了一个build-image.sh让“构建”这件事形成统一的入口。3.2 一个可落地的构建脚本设计下面这个脚本覆盖了大多数中小团队的需求镜像名作为必传参数版本号可以显式传也可以默认取当前时间戳构建完成后自动清理悬空镜像支持通过环境变量文件注入构建参数。#!/bin/bash # 用法: ./build-image.sh image_name [version] set -euo pipefail IMAGE_NAME${1:?缺少镜像名} VERSION${2:-$(date %Y%m%d%H%M%S)} WORKSPACE$(cd $(dirname ${BASH_SOURCE[0]}) pwd) echo [INFO] 开始构建 ${IMAGE_NAME}:${VERSION} echo [INFO] 构建目录: ${WORKSPACE} # 如果存在 .build_env 文件则加载其中的变量 if [ -f ${WORKSPACE}/.build_env ]; then export $(grep -v ^# ${WORKSPACE}/.build_env | xargs) fi # 正式构建 docker build \ --build-arg BUILD_TIME${VERSION} \ --build-arg APP_ENV${APP_ENV:-prod} \ -t ${IMAGE_NAME}:${VERSION} \ -t ${IMAGE_NAME}:latest \ ${WORKSPACE} # 构建完顺手清一波悬空镜像 docker image prune -f /dev/null echo [INFO] 构建完成: ${IMAGE_NAME}:${VERSION} docker images | grep ^${IMAGE_NAME}这里的几个设计决策我逐个解释默认版本取时间戳而不是用 latest。latest标签最大的问题是不可追溯——你无法通过latest知道当前线上跑的是哪次构建。而20240718153045这种时间戳自带排序能力拿来做默认版本号非常直观。脚本同时打上版本标签和latest标签兼顾了“可回滚”和“易获取”。.build_env文件支持环境差异化构建。在脚本目录放一个.build_env里面写APP_ENVstaging构建时就会通过--build-arg注入到 Dockerfile。多个环境就用多份脚本目录互不干扰。docker image prune -f在构建后清理悬空镜像。构建多次之后那些没有被打标签的中间层镜像会积累成垃圾。这一步能及时清理避免磁盘被吃满。需要说明的是这里我用的是image prune而不是system prune后者会连容器、网络一起清理误伤太大日常脚本里我从来不用。3.3 多架构构建一条命令同时出 amd64 和 arm64 镜像现在 ARM 设备越来越常见很多镜像需要同时支持 x86 和 ARM 架构。如果不用脚本就得手动执行docker buildx build --platform linux/amd64,linux/arm64 ...这个命令长而且容易打错。更好的方式是把 buildx 的调用也脚本化#!/bin/bash # 用法: ./buildx-multi.sh image_name version set -euo pipefail IMAGE_NAME${1:?缺少镜像名} VERSION${2:?缺少版本号} # 使用 buildx 进行多架构构建并直接推送 docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag ${IMAGE_NAME}:${VERSION} \ --push \ .这里要注意buildx 多架构构建要求 Docker 开启 containerd 镜像存储或者启用 BuildKit。如果直接报“no matching manifest”大概率是 builder 实例没创建或者 Docker 版本太旧。我在实践中用docker buildx create --use创建默认 builder 之后就没再遇到过问题。多架构构建通常是给“要推送到镜像仓库的公共镜像”用的如果是自建仓库或者内网部署一开始可以不做多架构等真有 ARM 机器再补不用为了追新而追新。3.4 镜像版本管理和仓库命名规范自动化之前先把规矩定好脚本能把命令标准化但不能替你决定命名规范。我强烈建议在写构建脚本之前先定好镜像仓库的命名规则。我自己常用的规范是[仓库地址]/[项目名]/[业务模块]:[版本]例如registry.internal.cn/order-system/gateway:20240718153045。有了这个规范之后批量部署脚本才能可靠地基于“项目名模块名”拼接出完整的镜像地址而不必每次问管理员仓库地址。你在热词里能看到“镜像安全 和容器安全”相关的内容命名规范其实就和安全相关——如果所有的业务镜像都堆在myapp/latest镜像的归属、来源、版本全都混乱审计根本无从谈起。另外补充一个安全习惯基础镜像不要随便用 latest尽量固定到具体版本甚至摘要digest。比如写FROM eclipse-temurin:17-jre-jammy而不是FROM openjdk:latest。latest基础镜像哪天被维护者强制覆盖你重新构建一次镜像里面什么都变了。把基础镜像版本固定在 Dockerfile 里是镜像可复现性的最后一道防线。4. 批量部署用 Shell for 循环配合 Docker Compose 干活4.1 批量的本质把“重复动作”交给循环批量部署的场景非常多开发环境里一次启动 8 个容器测试环境里 3 台服务器各部署同一套服务生产环境里把某条业务链路上的 5 个容器依次重新部署。不管是哪种场景手段都是一样的——用一个列表记录要操作的对象然后用 for 循环逐个处理。下面这个脚本是我的标准批量启动脚本它读取配置文件中的容器清单逐个检查、拉取、启停、启动#!/bin/bash # 容器清单文件格式: 每行一个 容器名:镜像名 # 部署思路: 拉取最新镜像 - 停止并删除旧容器 - 启动新容器 set -euo pipefail CONFIG_FILEcontainers.conf if [ ! -f $CONFIG_FILE ]; then echo [ERROR] 找不到配置文件 ${CONFIG_FILE} exit 1 fi while IFS: read -r name image; do # 跳过空行和注释 [ -z $name ] continue [[ $name \#* ]] continue echo 开始部署 ${name} (镜像: ${image}) # 1. 拉取最新镜像 docker pull $image # 2. 停止并删除旧容器如果存在 if docker ps -a --format {{.Names}} | grep -qx $name; then docker stop -t 15 $name docker rm $name fi # 3. 启动新容器 docker run -d \ --name $name \ --restart unless-stopped \ --network host \ $image echo [OK] ${name} 部署完成 done $CONFIG_FILE echo [INFO] 全部容器部署完成这段脚本里有一个热词场景的烙印--network host。就是“让容器使用宿主机网络”很多人在宝塔面板里遇到过“某个容器让它使用宿主机的网络环境”的诉求。用 host 模式之后容器不再单独分配虚拟网卡直接共享宿主机网络栈通过 localhost 就能访问端口也不用-p映射。它的优点是性能好、延迟低对依赖本地服务比如连宿主机上的 MySQL的应用很方便缺点是容器之间的网络隔离消失了端口容易冲突。生产环境我建议默认还是用 bridge 网络除非有明确的性能或特殊需求否则不要为了省事而全部 host。4.2 Shell 和 Docker Compose 的边界什么时候用 Compose什么时候用 Shell热词里也有docker compose我需要把这两者的分工说清楚因为这事我在团队里解释过很多次。Docker Compose 适合“单个项目内部的多服务编排”。它有依赖管理depends_on、有服务发现同名网络内直接解析服务名、有配置统一的 yaml 文件用docker compose up -d一条命令就能启动整个技术栈。这是单体项目落地时最好的伙伴。Shell 脚本适合“跨项目、跨机器、跨条件的胶水编排”。比如你要在 10 台服务器上部署 10 个不同的服务Compose 就无能为力了——它只能作用在单台机器的单个 Compose 项目上。而 Shell 配合 SSH、配合配置文件、配合循环可以轻易做到“每台机器各跑各的项目”。现实中最合理的组织方式通常是两者结合每个项目的服务编排用docker-compose.yml管跨服务器的部署、滚动更新、异常重试这些逻辑用 Shell 脚本管。脚本调用docker compose命令而不是重新造轮子。举个例子我部署某个 Java 项目时脚本只干三件事SSH 到目标服务器、git 拉代码、执行docker compose up -d --build。剩下的容器关系全部交给 compose 文件处理。4.3 滚动批量更新的思路别一次把所有容器全换成新的批量部署最怕的是“一刀切”尤其在生产环境。一次把 5 个容器全部停掉重建如果新镜像有严重问题服务就全挂了。所以我在批量脚本里加入了一个“滚动更新”模式逐个容器替换每个容器替换后等待健康检查通过再继续下一个。#!/bin/bash # 滚动更新模式: 逐个重建容器每个容器就绪后才处理下一个 set -euo pipefail for name in gateway order-pay user-center; do echo 开始更新 ${name} docker pull registry.internal.cn/order-system/${name}:latest # 重建单个容器 docker stop -t 15 $name 2/dev/null || true docker rm $name 2/dev/null || true docker run -d --name $name --network host registry.internal.cn/order-system/${name}:latest # 等待就绪 if ! wait_for_port 127.0.0.1 $PORT_MAP[$name]; then echo [ERROR] ${name} 更新后未通过健康检查脚本终止 exit 1 fi echo [OK] ${name} 更新完成${index}/${total} done为了保证滚动更新达到“不中断服务”的效果脚本执行前要把容器做成至少两个副本比如网关容器gateway和gateway-2脚本先把流量摘走重建gateway-2再把流量切回。这些逻辑在纯 Shell 里要做不少判断但思路是清晰的永远保持至少一个实例是可用的替换完一个再动下一个。如果你的服务入口有负载均衡Nginx 或云 LB让 LB 做摘流是最优雅的方案。4.4 定时批量任务的落地crontab 和脚本的最后一公里批量部署脚本写完之后给它挂一个定时任务是很自然的需求。例如每天凌晨 2 点清理悬空镜像和停止的容器每周日凌晨 3 点重建某些容易积累内存的容器。# crontab 示例 0 2 * * * /opt/scripts/cleanup-docker.sh /var/log/docker-cleanup.log 21 0 3 * * 0 /opt/scripts/weekly-restart.sh cache /var/log/docker-weekly.log 21crontab 执行脚本有两个容易踩的坑第一crontab 环境变量不全脚本里如果用到docker最好写上绝对路径或者在脚本开头export PATH/usr/local/bin:/usr/bin:/bin第二日志重定向务必加上否则脚本出错连个线索都没有。我这里把所有输出都落到日志文件里方便第二天排查。5. 实测踩坑实录Shell 和 Docker 组合的典型翻车现场5.1 Shell 层面的坑引号、CRLF 和 set -e 的误伤引号问题是 Shell 脚本新手最容易踩的。比如写for c in $(docker ps -aq)时如果某个容器名带空格虽然 Docker 不允许但脚本化后要穷举所有可能$(...)的结果会被词拆分。我的经验是凡是遍历一个数组或者命令结果一律加双引号写成for c in $或者for c in ${array[]}凡是要保存命令输出都用var$(command)不要写var$(command)裸奔。CRLF 坑可以说是 Windows 用户的必修课。我在 Windows 上写好的.sh脚本用 FTP 传到 Linux 服务器上执行报错信息永远是$\r: command not found。原因是 Windows 的换行是\r\n而 Linux 只需要\n多出来的\r被当成命令的一部分。解决方案有两个一是用dos2unix转一下dos2unix script.sh二是干脆在 VS Code 里把右下角的行尾序列从 CRLF 切换到 LF 再传。这个坑一旦遇到过往后写脚本都会长个记性。set -e 的误伤是个双刃剑。我开篇推荐set -euo pipefail但它在某些场景会好心办坏事。比如你想“容器不存在就创建”写了docker start $name || docker run -d ...如果docker start失败了因为 Shell 本身允许||右边的命令在左边失败后执行这在set -e下是没问题的不会退出。但如果你写的是docker start $name docker 运行其它命令且容器恰好不存在脚本就会直接退出后续命令全部不执行。处理这种场景我一般会在可能失败的指令后面显式加上|| true表示“这一步失败可以接受”避免误伤整体流程。5.2 Docker 层面的坑退出码 0 的假象、优雅停止超时、孤儿容器退出码 0 的假象是我觉得 Docker 自动化里最隐蔽的坑。docker run -d返回成功退出码是 0只代表容器被创建并启动调度了不代表容器内的进程正常。很多容器启动后立刻崩溃但因为入口进程退出了容器处于退出状态而docker run -d本身照样返回 0。这就是为什么我在启停脚本里强调“就绪检查”——TCP 探测、健康检查接口、日志关键字至少占一个。一个没有健康检查的部署脚本本质上是在“盲发”。优雅停止超时。不同应用对 SIGTERM 的处理差别很大有的应用收到信号做完整清理后退出有的应用压根忽略 SIGTERM必须靠 Docker 超时后发 SIGKILL。这就是为什么我反复强调docker stop -t参数——如果你不指定默认 10 秒对需要 30 秒清理状态的应用来说是远远不够的。我在停 MySQL 容器时遇到过因为没给足够的停止时间强制 kill 之后数据文件损坏恢复起来极其痛苦。从此-t 30成了我停数据库类容器的标配。孤儿容器。有时候部署脚本跑到一半报错旧容器停了新容器没起来或者反过来。这种半截状态最需要脚本具备幂等性——我无论执行多少次脚本都应该把系统恢复到最终期望的状态。上面批量部署脚本里“先查状态、存在就停、删、再启动”的逻辑本质上就是幂等设计。即使中途失败重跑脚本就能修复而不必人工去清理半吊子容器。5.3 一个完整排查案例容器启动后秒退我用一个真实案例展示完整的排查链路帮你在遇到同类问题时知道从哪里下手。场景脚本部署了一个 nginx 容器每次docker start之后过几秒容器就变成 Exited 状态。第一步先看退出码。docker inspect -f {{.State.ExitCode}} nginx如果退出码是 1说明是启动时应用自身报错如果是 0 但容器还是退出那大概率是进程主动退出。第二步看日志。docker logs nginx --tail 50。我那次排查日志最后一行是nginx: [emerg] host not found in upstream order-service——nginx 配置里有一个上游服务名在容器网络里解析不到。问题其实不在 nginx 本身而在容器网络配置。第三步查网络。docker network inspect bridge看容器是否在网络上、以及上游服务的容器是否也在同一个网络。结果发现上游服务容器没有启动所以 nginx 启动时做 DNS 解析失败直接退出。启动顺序错了。第四步修复。调整脚本让依赖服务先启动再启动 nginx。同时给 nginx 容器增加depends_on或者在脚本里等待上游服务的端口就绪。这四步排查过程看着简单但如果你没有“先看退出码、再看日志、再看网络”的固定套路很容易卡在“容器就是起不来”的泥潭里。把排查流程固化到自己的方法论里比记住任何单条命令都重要。5.4 自动化脚本的日常防护习惯踩过这些坑之后我现在写脚本会强制带几个习惯日志输出严格带时间戳和级别前缀。类似[INFO] 2024-07-18 15:30:45 开始构建 xxx这样出问题时能快速定位。关键步骤失败要即时退出但可容忍的失败要显式降级。通过|| true区分“绝对不能失败”和“失败也没关系”。脚本开头统一加export PATH/usr/local/bin:/usr/bin:/bin避免 crontab 等环境里找不到 docker 命令。所有脚本文件保存为 LF 换行并且文件开头写上用法注释。这些习惯单个看都不值钱但组合起来能救你很多次。6. 向自动化要收益脚本从“能用”到“好用”的进阶补全6.1 配置外置化别把服务器列表写死在脚本里再进阶一步批量部署脚本最该做的一件事就是“配置外置”。容器清单、服务器列表、镜像仓库地址这些经常变化的东西不应该硬编码在脚本里而应该抽到独立的配置文件或者环境变量。我在批量部署里用的是servers.conf每一行一个目标服务器的基本信息# 服务器清单: ip|ssh_port|服务标记 192.168.10.11|22|prod-web 192.168.10.12|22|prod-web 192.168.10.13|22|prod-cache脚本循环读这个文件用 SSH 在每个目标机器上执行预置的部署子脚本。这样新增一台机器只需要改配置文件不需要动脚本逻辑。Shell 的哲学就是这样——数据和逻辑分离逻辑固定数据自由。6.2 通知机制让自动化跑完自动“汇报”脚本跑完如果没人看到那自动化就没有闭环。我现在所有关键脚本跑完都会推一条通知到企业微信或者钉钉群用最简单的 curl 推送文本消息notify() { local webhook_urlhttps://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxxx curl -s -H Content-Type: application/json \ -d {\msgtype\:\text\,\text\:{\content\:\$1\}} \ $webhook_url /dev/null || true }部署成功、构建完成、健康检查失败都走这一个函数。因为|| true通知失败不会让部署脚本整体退出不会好心办坏事。这套通知机制加进去之后脚本才真正具备“无人值守”的价值——crontab 跑完你在手机上就知道结果了。6.3 与 CI/CD 平台的对接让 Jenkins 或 GitLab Runner 替你跑脚本当脚本稳定到一定程度之后下一步就是把它们挂到 CI/CD 平台上。Jenkins Pipeline 里可以直接写阶段调用脚本stage(构建镜像) { steps { sh ./scripts/build-image.sh order-gateway 20240718153045 } } stage(批量部署) { steps { sh ./scripts/batch-deploy.sh prod } }GitLab Runner 也一样在.gitlab-ci.yml里直接- ./scripts/build-image.sh xxx。到这里Shell 脚本就从“手动运维工具”变成了“持续交付流水线”的一个环节。整体收益是开发提交代码后构建、测试、部署全部自动完成人工的介入点只剩下“是否要触发发布”这一件事。6.4 个人经验总结把这套脚本沉淀成团队基础设施我最后想说的是Shell 结合 Docker 自动化的真正价值不在某一条命令也不在某一个花哨的语法而在于把运维经验沉淀成可以反复执行的东西。你今天手动敲的命令三个月后可能就忘了当时的参数但写进脚本里的每一个判断、每一个超时等待都带着你当时的思考。脚本就是运维知识的载体。在实际操作中我的体会是不要追求一步到位写出一个万能的自动化平台而是从一个容器、一个动作开始逐步积累。先把restart.sh写好再把build.sh写好再考虑批量部署最后挂到 CI。每一个脚本都在真实业务里跑过、踩过坑、修补过这样的脚本才可靠。反过来你第一天就想着写一套全自动发布平台大概率会变成又一个无人维护的废柴项目。一个小技巧送给大家给每个脚本文件头部写清“用途、用法、维护人、最近修改日期”四行注释。这个习惯一开始你觉得多余但半年后你翻自己的脚本库时会感谢当时的自己。
阅读完成 · 觉得有帮助?
咨询建站