统一部署脚本:一个 Shell 搞定前后端自动化部署
约 2816 字大约 9 分钟
2026-09-14
前两篇讲了部署方案的选型(从 Jenkins 到 Drone 再到 Cloud Toolkit)和各个部署文件的设计(Docker 容器化与脚本设计)。
这篇聊的是整个部署流程的最后一公里:把构建产物放到服务器上之后,怎么一键完成部署。答案就是这个 deploy.sh 统一部署脚本。
为什么需要一个统一脚本
没有这个脚本之前,每次部署都得手动执行一堆命令:
# 后端部署的手动流程(痛苦版)
docker build -t relax-web:latest .
docker stop relax-web
docker rm relax-web
docker run -d --name relax-web --restart unless-stopped \
-p 28031:8031 \
-v /data/relax-web/upload:/opt/relax-web/upload \
-v /data/relax-web/config:/opt/relax-web/config \
--network dualai \
-v /opt/docker/projects/relax-web/conf:/opt/relax-web/conf \
relax-web:latest
# 前端部署的手动流程(也不轻松)
rm -rf /opt/docker/nginx/html/bookmark_box
cp -r /opt/docker/projects/bookmark_box/dist /opt/docker/nginx/html/bookmark_box项目少的时候还行,项目一多就容易出错——漏了某步、端口写错、volume 路径不对……而且每个项目的部署命令都不一样,全靠脑子记。
统一脚本要解决的就是这个问题:不管什么项目,不管前端后端,跑一遍 deploy.sh 就完事。
脚本完整内容
先看完整代码,后面再逐段拆解:
#!/bin/bash
# 统一部署脚本:自动识别前端(dist)和后端(jar+Dockerfile)项目
PROJECTS_DIR="/opt/docker/projects"
NGINX_HTML="/opt/docker/nginx/html"
deploy_frontend() {
local project_dir="$1"
local project_name=$(basename "$project_dir")
local dist_path="$project_dir/dist"
local target_path="$NGINX_HTML/$project_name"
if [ ! -d "$NGINX_HTML" ]; then
echo "错误: Nginx HTML 目录不存在: $NGINX_HTML"
return 1
fi
echo "[前端] 发现项目: $project_name"
if [ -e "$target_path" ]; then
echo " 清理旧版本: $target_path"
rm -rf "$target_path"
fi
if mv "$dist_path" "$target_path"; then
echo " ✓ 部署成功: $target_path"
return 0
else
echo " ✗ 部署失败"
return 1
fi
}
deploy_backend() {
local project_dir="$1"
local project_name=$(basename "$project_dir")
echo "[后端] 发现项目: $project_name"
# 检查必要文件
if [ ! -f "$project_dir/Dockerfile" ]; then
echo " ✗ 缺少 Dockerfile,跳过"
return 1
fi
if ! ls "$project_dir"/*.jar 1>/dev/null 2>&1; then
echo " ✗ 缺少 jar 包,跳过"
return 1
fi
# 构建镜像
echo " 构建镜像: $project_name:latest"
if docker build -t "$project_name:latest" "$project_dir"; then
echo " ✓ 镜像构建成功"
# 停止并删除旧容器
docker stop "$project_name" 2>/dev/null
docker rm "$project_name" 2>/dev/null
# 加载环境变量 (.env)
local env_args=""
local env_file="$project_dir/.env"
if [ -f "$env_file" ]; then
echo " 加载环境变量: .env"
while IFS='=' read -r key value; do
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
env_args="$env_args -e $key=$value"
done < "$env_file"
fi
# 加载额外 docker run 参数 (deploy.conf)
local extra_run_args=""
local conf_file="$project_dir/deploy.conf"
if [ -f "$conf_file" ]; then
echo " 加载容器参数: deploy.conf"
extra_run_args=$(cat "$conf_file" | tr '\n' ' ')
fi
# 容器内工作目录 = /opt/<项目名>,日志挂载与之对应
local container_workdir="/opt/$project_name"
local log_volume="-v $project_dir/logs:${container_workdir}/logs"
# 启动新容器
echo " 启动容器: $project_name"
if docker run -d \
--name "$project_name" \
--restart unless-stopped \
$log_volume \
$env_args \
$extra_run_args \
"$project_name:latest"; then
echo " ✓ 容器启动成功"
# 清除 jar 包,防止二次部署
rm -f "$project_dir"/*.jar && echo " ✓ 已清除 jar 包"
return 0
else
echo " ✗ 容器启动失败"
return 1
fi
else
echo " ✗ 镜像构建失败"
return 1
fi
}
# 主流程
frontend_count=0
backend_count=0
for project_dir in "$PROJECTS_DIR"/*/; do
[ -d "$project_dir" ] || continue
# 检测前端项目
if [ -d "$project_dir/dist" ]; then
deploy_frontend "$project_dir" && ((frontend_count++))
# 检测后端项目
elif ls "$project_dir"/*.jar 1>/dev/null 2>&1 && [ -f "$project_dir/Dockerfile" ]; then
deploy_backend "$project_dir" && ((backend_count++))
fi
done
echo ""
echo "部署完成: 前端 $frontend_count 个, 后端 $backend_count 个"逐段拆解
项目类型自动识别
脚本的核心思路是约定优于配置——不需要你告诉它哪些是前端项目、哪些是后端项目,它自己看目录内容就能判断:
for project_dir in "$PROJECTS_DIR"/*/; do
[ -d "$project_dir" ] || continue
# 检测前端项目:有 dist/ 目录
if [ -d "$project_dir/dist" ]; then
deploy_frontend "$project_dir" && ((frontend_count++))
# 检测后端项目:有 *.jar 文件 + Dockerfile
elif ls "$project_dir"/*.jar 1>/dev/null 2>&1 && [ -f "$project_dir/Dockerfile" ]; then
deploy_backend "$project_dir" && ((backend_count++))
fi
done识别规则简单粗暴:
| 特征 | 判定为 | 部署方式 |
|---|---|---|
目录下有 dist/ | 前端项目 | 移动到 Nginx html 目录 |
目录下有 *.jar + Dockerfile | 后端项目 | Docker 构建 + 容器启动 |
| 都不满足 | 跳过 | 不处理 |
为什么用 elif 而不是 if ... if
一个项目不会同时是前端和后端。用 elif 避免了「既有 dist 又有 jar」这种理论上不该出现的情况被两边都执行一遍。实际上,如果真有这种目录,应该拆成两个独立项目。
前端部署逻辑
前端部署的逻辑非常直白:把 dist/ 目录移动到 Nginx 的 html 根目录下。
deploy_frontend() {
local project_dir="$1"
local project_name=$(basename "$project_dir") # 例如:bookmark_box
local dist_path="$project_dir/dist"
local target_path="$NGINX_HTML/$project_name" # 例如:/opt/docker/nginx/html/bookmark_box
# 清理旧版本
if [ -e "$target_path" ]; then
echo " 清理旧版本: $target_path"
rm -rf "$target_path"
fi
# 移动 dist 到目标位置
if mv "$dist_path" "$target_path"; then
echo " ✓ 部署成功: $target_path"
return 0
fi
}注意这里用的是 mv(移动)而不是 cp(复制),因为 dist/ 目录在部署之后就不需要了,移动比复制快,还省一次磁盘 IO。
📷 图片位(待补充)
应放什么图:前端部署前后的目录对比截图——部署前 dist/ 在 projects 目录下,部署后被移到了 nginx/html 下。
后端部署逻辑
后端部署是脚本的重头戏,分了五步走:
第一步:前置检查
# 检查必要文件
if [ ! -f "$project_dir/Dockerfile" ]; then
echo " ✗ 缺少 Dockerfile,跳过"
return 1
fi
if ! ls "$project_dir"/*.jar 1>/dev/null 2>&1; then
echo " ✗ 缺少 jar 包,跳过"
return 1
fi两个检查缺一不可:没有 Dockerfile 不知道怎么构建镜像,没有 jar 包构建出来也是空壳。ls *.jar 1>/dev/null 2>&1 这个写法有点绕——它用 ls 的通配符展开来检测是否存在 jar 文件,输出全部丢弃,只看退出码。
第二步:构建镜像
echo " 构建镜像: $project_name:latest"
if docker build -t "$project_name:latest" "$project_dir"; then
echo " ✓ 镜像构建成功"镜像标签统一用 <项目名>:latest,简单明了。
第三步:清理旧容器
# 停止并删除旧容器
docker stop "$project_name" 2>/dev/null
docker rm "$project_name" 2>/dev/null2>/dev/null 是为了抑制「容器不存在」时的报错信息。第一次部署的时候旧容器不存在,不重定向的话会输出一堆红色错误日志,看着吓人但其实没关系。
第四步:加载配置并启动
# 加载环境变量 (.env)
local env_args=""
local env_file="$project_dir/.env"
if [ -f "$env_file" ]; then
echo " 加载环境变量: .env"
while IFS='=' read -r key value; do
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
env_args="$env_args -e $key=$value"
done < "$env_file"
fi
# 加载额外 docker run 参数 (deploy.conf)
local extra_run_args=""
local conf_file="$project_dir/deploy.conf"
if [ -f "$conf_file" ]; then
echo " 加载容器参数: deploy.conf"
extra_run_args=$(cat "$conf_file" | tr '\n' ' ')
fi
# 日志 volume
local container_workdir="/opt/$project_name"
local log_volume="-v $project_dir/logs:${container_workdir}/logs"
# 启动新容器
docker run -d \
--name "$project_name" \
--restart unless-stopped \
$log_volume \
$env_args \
$extra_run_args \
"$project_name:latest"这段是整个脚本最精巧的部分。它从三个来源组装 docker run 的参数:
docker run -d --name <项目名> --restart unless-stopped
├── $log_volume ← 脚本自动生成(日志挂载)
├── $env_args ← 从 .env 文件读取(环境变量)
├── $extra_run_args ← 从 deploy.conf 读取(端口、网络、额外挂载)
└── <项目名>:latest ← 镜像名.env 文件的解析用了一个 while read 循环,逐行读取,跳过注释行(# 开头)和空行,把每一行转换成 -e KEY=VALUE 格式。
deploy.conf 的处理更简单粗暴——直接把文件内容读出来,用 tr '\n' ' ' 把换行符替换成空格,拼成一个长字符串。因为 deploy.conf 里每一行本身就是合法的 docker run 参数,拼起来就行。
第五步:清理 jar 包
# 清除 jar 包,防止二次部署
rm -f "$project_dir"/*.jar && echo " ✓ 已清除 jar 包"部署完成之后把 jar 包删掉,这是一个防御性设计——万一不小心又跑了一遍脚本,因为没有 jar 包了,后端项目会被自动跳过,不会触发一次无意义的重新部署。
扩展新项目有多简单
有了这个脚本,增加一个新项目只需要做两件事:
新增后端项目
在 /opt/docker/projects/ 下创建一个新目录,放入四个文件:
mkdir -p /opt/docker/projects/my-new-app
cd /opt/docker/projects/my-new-app
# 放入这些文件:
# ├── Dockerfile (镜像构建定义,参考 relax-web 的改)
# ├── start.sh (启动脚本,参考 relax-web 的改)
# ├── my-new-app.jar (应用包)
# ├── deploy.conf (端口映射和 volume 挂载)
# └── .env (环境变量,可选)
# 然后执行:
bash /opt/docker/deploy.sh脚本会自动发现这个新目录,识别为后端项目,执行 Docker 构建和启动。
新增前端项目
更简单:
mkdir -p /opt/docker/projects/my-frontend
# 把 dist/ 目录放进来
cp -r /path/to/dist /opt/docker/projects/my-frontend/
# 执行部署脚本
bash /opt/docker/deploy.sh脚本自动识别 dist/ 目录,把内容移动到 Nginx html 下。别忘了在 Nginx 配置里加上对应的 location 块。
📷 图片位(待补充)
应放什么图:完整部署流程的终端截图——从执行 bash deploy.sh 到看到「部署完成: 前端 X 个, 后端 X 个」的全部输出。
部署脚本的使用方式
全量部署
直接执行,脚本会遍历 projects/ 下所有项目:
bash /opt/docker/deploy.sh注意
全量部署会处理所有项目。如果你只想部署某一个项目,需要把其他项目的 dist/ 或 *.jar 先移走(或者临时改名),让脚本识别不到它们。后续可以给脚本加一个参数来指定单个项目部署,目前还没做这个功能。
只部署某一个项目(临时方案)
当前版本没有参数化支持,但可以用一个小技巧:
# 只部署后端项目 relax-web
# 先把其他项目的部署文件移走,或者直接用 docker 命令手动操作:
cd /opt/docker/projects/relax-web
docker build -t relax-web:latest .
docker stop relax-web && docker rm relax-web
# 然后手动拼 docker run 命令(参数从 deploy.conf 和 .env 里来)后续改进方向
给脚本加一个项目名参数,比如 bash deploy.sh relax-web,只部署指定项目。这个改进很小,后面有空了会加上。
小结
这个统一部署脚本不到 120 行,但覆盖了部署流程的所有关键环节:
| 能力 | 实现方式 |
|---|---|
| 自动识别前端/后端 | 检查 dist/ 目录和 *.jar + Dockerfile |
| 前端部署 | dist/ 移动到 Nginx html 目录 |
| 后端部署 | Docker 构建 → 清理旧容器 → 加载配置 → 启动新容器 |
| 环境变量管理 | 自动读取 .env 文件 |
| 容器参数管理 | 自动读取 deploy.conf 文件 |
| 日志持久化 | 自动挂载日志 volume |
| 防重复部署 | 部署后自动清除 jar 包 |
| 部署统计 | 输出前端/后端各部署了多少个项目 |
整个部署系列到这里就讲完了。 回顾一下三篇文章的脉络:
- 选型:为什么 Docker + Cloud Toolkit,而不是 Jenkins 或 Drone
- 设计:Dockerfile、start.sh、deploy.conf、配置文件分层管理的每个细节
- 自动化:一个 120 行的 Shell 脚本把前后端部署全部搞定
对个人项目来说,这套方案够用、轻量、好维护。等服务器的配置上去了、项目数量多起来了,再考虑 Docker Compose 或者 K8s 也不迟。现阶段——能跑、好使、不折腾,就是最好的方案。
📷 图片位(待补充)
应放什么图:部署成功后的全景截图——docker ps 展示所有运行中的容器 + Nginx 配置展示前端项目的路由规则 + 浏览器访问效果。