1. 项目概述与核心价值
最近在折腾一个内部项目,每次更新代码都要手动登录服务器、拉取镜像、停止旧容器、启动新容器,一套流程下来,少说也得三五分钟,还容易手滑出错。这种重复性劳动干多了,就琢磨着能不能写个脚本把这活儿给自动化了。这不只是偷懒,更是为了流程的标准化和可靠性。一个可靠的自动部署脚本,能确保每次部署的动作都一模一样,避免了人为失误,尤其是在需要快速回滚或者多环境部署时,它的价值就凸显出来了。
这个脚本的核心目标很明确:实现Docker镜像从仓库到生产环境的一键式、无人值守部署。它要能自动处理拉取最新镜像、优雅地替换运行中的容器、清理无用资源这一系列操作。听起来简单,但里面有不少细节需要考虑,比如如何保证服务在更新期间不间断(零停机或最短停机)、如何处理部署失败的回滚、以及如何与现有的CI/CD流程(比如Jenkins、GitLab CI)无缝集成。对于运维工程师、后端开发者,甚至是独立开发者管理自己的小项目,掌握编写这样一个脚本的技能,都能极大提升效率和应用交付的稳定性。
2. 脚本整体设计与思路拆解
在动手写代码之前,得先想清楚脚本的“行动路线图”。一个健壮的自动部署脚本,不能只是几条Docker命令的简单堆砌,它需要像一个老练的舵手,知道在什么风浪下采取什么动作。
2.1 核心流程与状态机思维
我把整个部署过程抽象成一个状态机,脚本需要清晰地感知并管理每个状态:
- 预备状态:检查环境是否就绪(Docker服务是否运行,所需镜像仓库是否可访问)。
- 拉取状态:从指定的镜像仓库拉取目标镜像。这里要考虑网络超时、认证失败等情况。
- 更新状态:这是最核心也最需谨慎的一步。需要停止旧容器,并用新镜像启动新容器。关键是如何做到平滑过渡。
- 清理状态:部署成功后,清理掉旧的、无用的Docker镜像,释放磁盘空间。
- 回滚状态:当上述任何一步失败时,脚本应能自动或手动触发回滚到上一个稳定版本。
基于这个状态机,脚本的基本流程骨架就出来了:检查 -> 拉取 -> 更新 -> 清理 -> (异常)回滚。
2.2 技术方案选型:Shell脚本为何是首选
实现这样一个脚本,有多种语言可以选择,比如Python、Go,甚至用Ansible这样的配置管理工具。但我最终选择了Shell脚本(特别是Bash),原因有几个:
- 环境普适性:绝大多数Linux服务器和CI/CD环境(如Jenkins的Shell执行器)都原生支持Bash,无需额外安装运行时环境。
- 与Docker命令行的无缝集成:Docker CLI本身就是命令行工具,用Shell脚本调用它是最直接、最自然的方式,可以方便地处理命令输出和退出码。
- 轻量与高效:对于这种以执行为主的任务,Shell脚本启动快,资源占用小,没有复杂的依赖。
- 强大的文本处理能力:部署中经常需要解析镜像标签、容器ID等信息,
grep,awk,sed等工具能轻松应对。
当然,Shell脚本也有缺点,比如错误处理相对繁琐,复杂数据结构支持弱。但对于我们这个主要逻辑是顺序执行命令的场景,它的优点远大于缺点。对于更复杂的部署逻辑(如多服务编排、蓝绿部署),可以考虑用Python增强,但核心的Docker操作依然可以封装在Shell函数中。
2.3 关键设计决策:平滑更新与健康检查
如何实现“平滑更新”是脚本设计的重中之重。粗暴的docker stop && docker run会导致服务短暂不可用。更优的方案是:
- 使用
docker-compose或docker run的--restart策略:但这主要用于容器崩溃后的重启,对于主动更新不完美。 - 推荐方案:先启动新容器,再停止旧容器:这是实现零停机或极短停机时间的常用模式。脚本可以:
- 用新镜像启动一个临时的新容器(通常需要映射到不同的临时端口,或使用相同的网络)。
- 等待新容器通过健康检查(
HEALTHCHECK指令或自定义脚本检查应用端口/接口)。 - 一旦新容器健康,将流量切换过来(这可能涉及负载均衡器配置,脚本可以调用API)。
- 最后停止并移除旧容器。
对于没有复杂流量切换的内部服务,一个简化的可靠做法是:利用Docker容器的网络别名和服务的重试机制。不过,在我们的基础脚本中,我们会先实现标准的“先停后启”并确保成功,再探讨更高级的模式。同时,必须为每一个Docker命令添加充分的错误检查,任何一步失败都应中止流程并记录日志。
3. 脚本核心细节解析与实操要点
3.1 环境变量与配置管理
把配置硬编码在脚本里是坏习惯。我们需要通过环境变量或配置文件来让脚本变得可复用。主要配置项包括:
IMAGE_NAME:完整的镜像地址,如registry.mycompany.com/myapp:latest或nginx:alpine。CONTAINER_NAME:运行中容器的名称,用于定位和操作旧容器。DOCKER_REGISTRY_AUTH:私有仓库的认证信息(注意安全,建议从外部文件读取或使用Docker已配置的credential helper)。BACKUP_TAG:用于标记旧镜像的标签,便于回滚。
在脚本开头,我们可以这样设置默认值并允许外部覆盖:
#!/bin/bash # 配置项(可通过环境变量覆盖) IMAGE_NAME=${DEPLOY_IMAGE:-"myapp:latest"} CONTAINER_NAME=${DEPLOY_CONTAINER:-"myapp-container"} DOCKER_NETWORK=${DOCKER_NETWORK:-"bridge"} RESTART_POLICY=${RESTART_POLICY:-"always"}注意:对于密码等敏感信息,绝对不要直接写在脚本或环境变量中(容易被
ps命令看到)。应使用Docker的--env-file参数从文件加载,或使用专门的密钥管理服务。
3.2 镜像拉取策略与标签管理
直接拉取latest标签是最简单的,但不利于版本追踪和回滚。生产环境强烈建议使用明确的版本标签,例如myapp:v1.2.3。脚本可以接受一个TAG参数。
拉取镜像时,要考虑网络问题:
echo "正在拉取镜像 $IMAGE_NAME..." if ! docker pull $IMAGE_NAME; then echo "错误:拉取镜像失败!" exit 1 fi如果使用私有仓库,需要确保执行脚本的用户已经通过docker login认证,或者脚本中包含认证步骤。
3.3 容器更新策略:停止、移除、重启
这是核心操作。我们需要找到正在运行的旧容器。不能简单地用docker run启动新容器,因为如果同名容器已存在,docker run会失败。
# 检查旧容器是否存在 OLD_CONTAINER_ID=$(docker ps -q -f name=$CONTAINER_NAME) if [ -n "$OLD_CONTAINER_ID" ]; then echo "发现正在运行的旧容器,ID: $OLD_CONTAINER_ID" echo "正在停止旧容器..." docker stop $CONTAINER_NAME if [ $? -eq 0 ]; then echo "正在移除旧容器..." docker rm $CONTAINER_NAME else echo "警告:停止容器失败,尝试强制移除..." docker rm -f $CONTAINER_NAME fi fi关键点:
docker ps -q -f name=可以精准地通过名称查找容器。- 先
stop再rm是优雅的方式,给容器进程发送SIGTERM信号,允许它完成收尾工作。 - 如果停止失败(比如容器卡住),再使用
rm -f强制移除。 - 移除旧容器是必要的,否则新容器无法使用相同的名称启动。
3.4 新容器启动与端口映射
启动新容器时,需要把旧容器所有的运行参数“复制”过来。如果你之前是用docker run启动的,需要记录下所有参数(端口映射-p、数据卷-v、环境变量-e等)。这也是为什么推荐使用docker-compose.yml来定义服务的原因——所有配置都在一个文件里,更新时只需重新docker-compose up -d。
在Shell脚本中,如果参数固定,可以这样写:
echo "正在启动新容器..." docker run -d \ --name $CONTAINER_NAME \ --network $DOCKER_NETWORK \ --restart=$RESTART_POLICY \ -p 8080:80 \ -v /app/config:/config \ -e "ENV=production" \ $IMAGE_NAME实操心得:对于复杂的启动命令,强烈建议将这部分参数提取到单独的变量或配置文件中。更好的做法是,从一开始就使用Docker Compose,那么部署脚本就简化为docker-compose pull && docker-compose up -d。
3.5 健康检查与部署验证
容器启动成功不代表应用服务就绪。我们需要添加健康检查。
- 方法一:利用Docker原生HEALTHCHECK。如果镜像内已定义,可以使用
docker inspect --format='{{.State.Health.Status}}' $CONTAINER_NAME来轮询状态,直到变为healthy。 - 方法二:脚本轮询。对于没有定义HEALTHCHECK的镜像,可以写一个循环,用
curl、nc(netcat) 或wget来检查应用特定的端口或健康检查端点。
echo "等待应用就绪..." MAX_RETRIES=30 RETRY_INTERVAL=5 for i in $(seq 1 $MAX_RETRIES); do if curl -f http://localhost:8080/health > /dev/null 2>&1; then echo "应用健康检查通过!" break fi echo "应用未就绪,等待 ${RETRY_INTERVAL}秒后重试... ($i/$MAX_RETRIES)" sleep $RETRY_INTERVAL if [ $i -eq $MAX_RETRIES ]; then echo "错误:应用在超时时间内未就绪,部署可能失败!" exit 1 fi done3.6 旧镜像清理策略
每次部署都会拉取新镜像,旧镜像会占用磁盘空间。需要定期清理。可以在部署成功后进行:
echo "清理未被使用的旧镜像..." docker image prune -fdocker image prune -f会删除所有未被任何容器引用的悬空镜像(<none>)。如果你想删除所有未被使用的镜像(包括有标签但未使用的),可以加-a参数,但要格外小心,避免删除了其他服务依赖的镜像。更安全的做法是只删除特定镜像的旧版本:
# 删除当前镜像之外的所有旧版本(假设标签为版本号) CURRENT_IMAGE_HASH=$(docker images --format "{{.ID}}" $IMAGE_NAME | head -n1) docker images $IMAGE_NAME | grep -v $CURRENT_IMAGE_HASH | awk 'NR>1 {print $3}' | xargs -r docker rmi这个命令稍微复杂,它获取当前使用镜像的ID,然后删除同名镜像中ID不是它的其他镜像。
4. 完整脚本实现与逐行解读
下面是一个整合了上述考量的、相对完整的自动部署脚本示例。它包含了错误处理、日志记录和基本的回滚能力。
#!/bin/bash # ============================================ # 自动部署Docker镜像脚本 # 使用方法: ./deploy.sh [镜像名:标签] # 例如: ./deploy.sh myregistry.com/app:v1.2.0 # ============================================ set -euo pipefail # 严格模式:遇到错误退出,未定义变量报错,管道中任意错误视为失败 # ---------- 配置区 ---------- # 这些变量可以被环境变量覆盖 TARGET_IMAGE="${1:-}" # 从第一个参数获取镜像名 CONTAINER_NAME="${DEPLOY_CONTAINER_NAME:-myapp-container}" DOCKER_NETWORK="${DOCKER_NETWORK:-bridge}" RESTART_POLICY="${RESTART_POLICY:-always}" # 端口映射 (格式: "主机端口:容器端口") PORT_MAPPING="${PORT_MAPPING:-8080:80}" # 数据卷映射 (格式: "主机路径:容器路径") VOLUME_MAPPING="${VOLUME_MAPPING:-/host/data:/app/data}" # 环境变量文件(可选) ENV_FILE="${ENV_FILE:-.env.production}" # 健康检查URL(可选) HEALTH_CHECK_URL="${HEALTH_CHECK_URL:-http://localhost:8080/health}" # 日志文件 LOG_FILE="deploy_$(date +%Y%m%d_%H%M%S).log" # ---------- 函数定义 ---------- log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } error_exit() { log "错误: $1" exit 1 } check_dependency() { if ! command -v docker &> /dev/null; then error_exit "Docker未安装或不在PATH中。" fi if ! docker info &> /dev/null; then error_exit "Docker守护进程未运行或无权限访问。" fi } pull_image() { local image=$1 log "开始拉取镜像: $image" if docker pull "$image"; then log "镜像拉取成功。" # 获取刚拉取镜像的完整ID NEW_IMAGE_ID=$(docker images --quiet "$image" | head -n1) echo "$NEW_IMAGE_ID" > .last_image_id else error_exit "镜像拉取失败,请检查网络或镜像地址。" fi } stop_and_remove_container() { local container_name=$1 if docker ps -a --format "{{.Names}}" | grep -q "^${container_name}$"; then log "发现容器 '$container_name',正在停止..." if docker stop "$container_name"; then log "容器停止成功,正在移除..." docker rm "$container_name" && log "容器移除成功。" else log "警告:正常停止容器失败,尝试强制移除..." docker rm -f "$container_name" && log "容器已强制移除。" fi else log "未找到名为 '$container_name' 的容器,可能为首次部署。" fi } start_new_container() { local image=$1 local name=$2 log "正在启动新容器 '$name'..." # 构建docker run命令 local run_cmd="docker run -d --name $name --network $DOCKER_NETWORK --restart=$RESTART_POLICY" # 添加端口映射 if [ -n "$PORT_MAPPING" ]; then run_cmd="$run_cmd -p $PORT_MAPPING" fi # 添加数据卷映射 if [ -n "$VOLUME_MAPPING" ]; then run_cmd="$run_cmd -v $VOLUME_MAPPING" fi # 添加环境变量文件 if [ -f "$ENV_FILE" ]; then run_cmd="$run_cmd --env-file $ENV_FILE" log "使用环境变量文件: $ENV_FILE" fi # 最后加上镜像名 run_cmd="$run_cmd $image" log "执行命令: $run_cmd" if eval "$run_cmd"; then log "新容器启动命令执行成功。" else error_exit "启动新容器失败!" fi } health_check() { local url=$1 local max_retries=30 local retry_interval=5 local container_name=$CONTAINER_NAME log "等待容器应用就绪,健康检查URL: $url" # 首先检查容器状态是否为运行中 for i in $(seq 1 10); do if [ "$(docker inspect -f '{{.State.Status}}' "$container_name" 2>/dev/null)" = "running" ]; then log "容器状态已为 'running'。" break fi sleep 2 if [ $i -eq 10 ]; then error_exit "容器未能进入运行状态。" fi done # 进行应用层健康检查 for i in $(seq 1 $max_retries); do # 使用curl检查健康端点,设置超时 if curl -s -f --max-time 5 "$url" > /dev/null 2>&1; then log "健康检查通过!应用已完全就绪。" return 0 fi log "健康检查未通过 ($i/$max_retries),${retry_interval}秒后重试..." sleep $retry_interval done error_exit "应用在预设时间内未通过健康检查,部署可能有问题。" } cleanup_old_images() { local current_image=$1 log "开始清理未被使用的旧镜像..." # 删除所有悬空镜像 docker image prune -f # 可选:删除同一仓库中非当前使用的其他标签镜像(谨慎操作) # 这里暂时注释掉,因为需要更精确的逻辑 # log "基础清理完成。" } rollback() { log "========== 尝试回滚 ==========" local last_image_id if [ -f .last_image_id ]; then last_image_id=$(cat .last_image_id) log "找到上一次部署的镜像ID: $last_image_id" # 停止当前可能启动失败的新容器 stop_and_remove_container "$CONTAINER_NAME" # 用旧镜像启动容器 log "尝试用旧镜像启动容器..." if docker run -d --name $CONTAINER_NAME --network $DOCKER_NETWORK --restart=$RESTART_POLICY -p $PORT_MAPPING $last_image_id; then log "回滚成功,容器已用旧镜像启动。" else log "回滚启动失败,请手动检查。" fi else log "未找到上一次部署的镜像记录,无法自动回滚。" fi } # ---------- 主流程 ---------- main() { log "========== Docker自动部署脚本开始 ==========" # 0. 参数检查 if [ -z "$TARGET_IMAGE" ]; then error_exit "请指定要部署的Docker镜像名和标签。\n示例: $0 nginx:alpine" fi log "目标镜像: $TARGET_IMAGE" log "容器名称: $CONTAINER_NAME" # 1. 依赖检查 check_dependency # 2. 拉取新镜像 pull_image "$TARGET_IMAGE" # 3. 停止并移除旧容器 stop_and_remove_container "$CONTAINER_NAME" # 4. 启动新容器 start_new_container "$TARGET_IMAGE" "$CONTAINER_NAME" # 5. 健康检查 if [ -n "$HEALTH_CHECK_URL" ]; then health_check "$HEALTH_CHECK_URL" else log "未设置健康检查URL,跳过健康检查。" sleep 10 # 即使不检查,也等待一小段时间让容器初始化 fi # 6. 清理旧镜像 cleanup_old_images "$TARGET_IMAGE" log "========== 部署成功完成! ==========" } # 执行主函数,并设置陷阱以捕获错误进行回滚 trap rollback ERR INT TERM main脚本解读与关键点:
set -euo pipefail:这是编写健壮Shell脚本的黄金法则。-e让脚本在任何命令失败时立即退出;-u遇到未定义的变量时报错;-o pipefail确保管道命令中任意环节失败,整个管道都视为失败。- 灵活的配置:所有关键参数都通过变量定义,并优先从环境变量读取,使得脚本高度可配置。
- 模块化函数:将每个步骤封装成函数(
log,pull_image,stop_and_remove_container等),主流程清晰,也便于单独测试和复用。 - 完整的错误处理:每个关键操作后都检查命令返回值 (
$?)。主流程被trap rollback ERR INT TERM包裹,这意味着如果脚本因错误 (ERR) 退出,或被用户中断 (INT),或收到终止信号 (TERM),都会自动触发rollback函数尝试回滚。 - 回滚机制:在成功拉取新镜像后,将其ID保存到
.last_image_id文件。如果后续步骤失败,回滚函数会读取这个ID,尝试用旧镜像重新启动服务。这是一种简单的回滚,更复杂的系统可能需要维护一个完整的旧镜像标签列表。 - 健康检查:提供了容器状态检查和应用层检查(HTTP)两种方式,确保应用真正可用。
- 日志记录:
log函数将所有输出同时打印到屏幕和日志文件,便于事后审计和排查问题。
5. 进阶优化与集成实践
基础脚本能跑通,但要用于生产环境,还需要考虑更多。
5.1 与CI/CD工具集成(以Jenkins为例)
这个脚本可以完美地集成到Jenkins Pipeline中。在Jenkinsfile中,可以这样调用:
pipeline { agent any environment { DEPLOY_IMAGE = 'my-registry.com/myapp:${BUILD_NUMBER}' DEPLOY_CONTAINER_NAME = 'myapp-prod' } stages { stage('Build and Push') { steps { sh 'docker build -t ${DEPLOY_IMAGE} .' sh 'docker push ${DEPLOY_IMAGE}' } } stage('Deploy to Production') { steps { // 将部署脚本传到目标服务器并执行 sshagent(['production-server-credentials']) { sh """ scp deploy.sh user@production-server:/tmp/ ssh user@production-server "cd /tmp && chmod +x deploy.sh && ./deploy.sh ${DEPLOY_IMAGE}" """ } } } } post { failure { // 可以在这里触发更复杂的通知或回滚流程 echo '构建或部署失败!' } } }集成要点:
- 凭证管理:使用Jenkins的Credentials插件管理SSH私钥或Docker仓库密码。
- 参数化构建:将镜像标签作为构建参数传入,实现灵活部署。
- 流水线可视化:清晰的stage划分让部署过程一目了然。
5.2 实现蓝绿部署或金丝雀发布
对于要求高可用的服务,可以扩展脚本支持更高级的部署策略。
蓝绿部署思路:
- 准备两套完全相同的环境:“蓝环境”(当前生产)和“绿环境”(待上线)。
- 脚本部署新版本到“绿环境”,并进行充分测试。
- 测试通过后,将负载均衡器(如Nginx、HAProxy)的流量从“蓝环境”切换到“绿环境”。
- 切换成功后,“蓝环境”变为空闲,可用于下一次部署。
脚本需要管理两套容器(如myapp-blue,myapp-green)和更新负载均衡器配置(可通过调用API或修改配置文件并重载)。
# 伪代码逻辑 CURRENT_COLOR=$(get_current_color_from_lb) # 从负载均衡器获取当前颜色 if [ "$CURRENT_COLOR" = "blue" ]; then NEXT_COLOR="green" OLD_CONTAINER="myapp-blue" NEW_CONTAINER="myapp-green" else NEXT_COLOR="blue" OLD_CONTAINER="myapp-green" NEW_CONTAINER="myapp-blue" fi # 部署新版本到 NEW_CONTAINER deploy_to_container $NEW_CONTAINER $NEW_IMAGE # 健康检查 health_check_container $NEW_CONTAINER # 切换负载均衡器流量 switch_load_balancer $NEXT_COLOR # 下线旧容器 stop_and_remove_container $OLD_CONTAINER5.3 使用Docker Compose简化管理
如果服务涉及多个容器(如应用+数据库+缓存),或者启动参数复杂,强烈建议使用Docker Compose。部署脚本会变得极其简单:
#!/bin/bash # deploy-with-compose.sh set -e cd /path/to/your/app # 拉取最新镜像 docker-compose pull # 重新创建并启动容器(会停止旧容器) docker-compose up -d --force-recreate # 执行健康检查 ./health-check.sh # 清理旧镜像 docker image prune -fdocker-compose up -d本身就会处理容器的创建、启动和更新,--force-recreate确保即使配置未变也重新创建容器。管理多服务应用,Docker Compose是比纯Shell脚本更优雅的选择。
5.4 安全性增强
- 最小权限原则:不要用root用户运行脚本或Docker守护进程。可以考虑创建一个专门的
deploy用户,并配置其Docker权限(将其加入docker用户组)。 - 镜像签名与验证:在拉取镜像后,可以使用Docker Content Trust (DCT) 验证镜像的签名,确保镜像来源可信且未被篡改。
- 秘密管理:使用Docker Secrets(在Swarm模式下)或第三方工具如HashiCorp Vault来管理数据库密码、API密钥等敏感信息,而不是通过环境变量传递。
6. 常见问题与排查技巧实录
在实际使用中,你肯定会遇到各种问题。下面是我踩过的一些坑和解决方法。
6.1 部署失败常见原因速查表
| 问题现象 | 可能原因 | 排查命令与解决方法 |
|---|---|---|
docker pull失败 | 1. 网络不通 2. 镜像不存在 3. 私有仓库未认证 | docker login <registry>ping <registry-host>curl -k https://<registry>/v2/ |
docker run失败,提示端口被占用 | 旧容器未完全停止或端口被其他进程占用 | netstat -tlnp | grep :<端口号>docker ps -a查看容器状态,docker rm -f强制移除 |
docker run失败,提示容器名冲突 | 同名容器已存在(可能是之前失败的残留) | docker ps -a | grep <容器名>先执行 docker rm -f <容器名> |
| 容器启动后立即退出 | 1. 应用启动错误 2. 缺少必要环境变量或卷 | docker logs <容器名>查看日志docker run -it <镜像> sh进入镜像检查 |
| 健康检查始终不通过 | 1. 应用启动慢 2. 健康检查端点不对 3. 网络策略限制 | 增加MAX_RETRIES和RETRY_INTERVALdocker exec <容器名> curl localhost:<容器端口>/health检查容器网络模式与防火墙 |
| 磁盘空间不足 | 过多无用镜像和容器占用空间 | docker system df查看磁盘使用docker system prune -a(谨慎!) 清理所有未用资源 |
6.2 实战调试技巧
- 查看实时日志:部署过程中,另开一个终端用
docker logs -f <容器名>跟踪应用日志,能第一时间发现启动错误。 - 进入容器调试:如果应用启动异常,可以用
docker exec -it <容器名> /bin/sh进入容器内部,检查文件、进程和环境变量。 - 检查Docker守护进程:如果所有Docker命令都报错,可能是Docker服务挂了。用
systemctl status docker(Systemd) 或service docker status(SysVinit) 检查状态。 - “镜像拉取慢”问题:国内拉取Docker Hub镜像很慢。修改Docker守护进程配置,使用国内镜像加速器。编辑
/etc/docker/daemon.json,加入:
然后重启Docker:{ "registry-mirrors": [ "https://registry.docker-cn.com", "https://mirror.ccs.tencentyun.com" ] }sudo systemctl restart docker。 - 脚本权限与格式:确保脚本有执行权限 (
chmod +x deploy.sh),并且脚本文件格式是Unix (LF),而不是Windows (CRLF),否则可能在Linux上执行出错。可以用dos2unix deploy.sh转换。
6.3 我踩过的几个印象深刻的坑
- 变量引用缺失引号导致路径中有空格时脚本崩溃:早期写
docker run -v $DATA_PATH,如果DATA_PATH包含空格,命令会被错误分割。务必给变量加双引号:docker run -v "$DATA_PATH"。 - 未处理
docker stop的超时:默认docker stop会给容器10秒优雅停止时间,然后发送SIGKILL。对于关闭缓慢的应用,这可能导致数据不一致。可以用docker stop -t 30增加超时时间,并在脚本中根据应用特性调整。 - 在CI/CD中误用
latest标签:Jenkins Pipeline中多次使用:latest构建镜像,导致部署时无法确定服务器上拉取到的到底是不是刚刚构建的那一个。一定要使用唯一标签,如构建ID (${BUILD_NUMBER}) 或Git提交哈希 (${GIT_COMMIT:0:8})。 - 清理镜像时误删正在使用的镜像:早期鲁莽地使用
docker rmi $(docker images -q),结果把其他正在运行的服务的基础镜像给删了。清理时必须格外小心,最好只清理悬空镜像 (docker image prune),或者精确指定要删除的镜像名和标签。
编写自动部署脚本是一个从“能用”到“好用”再到“可靠”的迭代过程。最开始可能只是一个简单的命令集合,随着你遇到越来越多边界情况和生产环境的需求,你会不断加入错误处理、日志、健康检查、回滚等机制。这个脚本最终会成为你基础设施中不可或缺的、可靠的一环。记住,好的自动化脚本不是为了炫技,而是为了让你能睡个安稳觉。