前后端部署实战:Docker 容器化与脚本设计
约 3473 字大约 12 分钟
2026-09-14
上一篇 部署三部曲:从 Jenkins 到 Drone 再到 Cloud Toolkit 聊了部署方案的选型过程,最终定下来的是:本地构建 + Cloud Toolkit 传输 + 服务器统一部署脚本。
这篇讲具体怎么落地。我会把每个部署文件掰开了讲——后端需要哪些文件、每个文件干什么、为什么这么设计;前端怎么部署;以及一次完整的部署操作长什么样。
先看全景:一次部署需要什么
后端项目(以 relax-web 为例)部署到服务器,总共需要这些文件:
| 文件 | 作用 | 是否必需 |
|---|---|---|
Dockerfile | 镜像构建定义 | ✅ 必需 |
start.sh | Java 启动逻辑 | ✅ 必需 |
*.jar | 应用包 | ✅ 必需 |
.env | 环境变量覆盖 | ❌ 可选 |
deploy.conf | 额外 docker run 参数 | ❌ 可选 |
conf/ | Spring 配置文件目录 | ✅ 必需 |
前端项目更简单,只需要一个 dist/ 目录(构建产物),直接放到 Nginx 的 html 目录下就行。
📷 图片位(待补充)
应放什么图:/opt/docker/projects/ 目录下的实际文件列表截图(ls -la 输出),让读者看到真实的部署目录长什么样。
后端部署:四个文件搞定
Dockerfile —— 镜像怎么建
这是后端项目的 Dockerfile,每一行都有注释,先看完整内容:
FROM eclipse-temurin:21-jre-alpine
# 工作目录使用项目名,与容器名保持一致
WORKDIR /opt/relax-web
COPY start.sh /opt/relax-web/start.sh
COPY *.jar /opt/relax-web/
RUN chmod +x /opt/relax-web/start.sh
# 默认环境变量(可通过 .env 覆盖)
ENV JAVA_OPTS="-Xms128m -Xmx128m" \
JAR_PATH="/opt/relax-web" \
EXAM_ENV="prod" \
LOG="true"
EXPOSE 8031
ENTRYPOINT ["/opt/relax-web/start.sh"]逐行解读:
FROM eclipse-temurin:21-jre-alpine —— 基础镜像选型。这里有两个讲究:
eclipse-temurin而不是openjdk:Eclipse Temurin 是 Eclipse 基金会维护的 OpenJDK 发行版,是目前社区最主流的免费 JDK 镜像。老版的openjdk官方镜像已经停止更新了21-jre-alpine而不是21-jdk:生产环境只需要 JRE(运行),不需要 JDK(开发)。Alpine 版本基于 Alpine Linux,镜像体积只有几十 MB,比 Debian 版小了一大截。对低配服务器来说,镜像越小越好——占磁盘少,拉取快,启动也快
关于 JRE vs JDK
如果你用到了 jshell、javac 这些开发工具,或者某些框架在运行时需要 JDK(比如某些热部署插件),那就得用 jdk 版本。绝大多数 Spring Boot 应用只需要 jre。
WORKDIR /opt/relax-web —— 容器内的工作目录。用项目名命名,和容器名保持一致,这样 docker exec -it relax-web sh 进去之后就在正确的位置。
COPY start.sh + COPY *.jar —— 把启动脚本和 jar 包复制进镜像。注意这里用的是通配符 *.jar,不写死 jar 包的名字(因为 Maven 打包出来的 jar 名可能带版本号)。
ENV JAVA_OPTS="-Xms128m -Xmx128m" —— JVM 内存参数的默认值。2G 内存的服务器,给 Java 进程 128MB 已经够用了。这个值可以通过 .env 文件覆盖,后面讲。
ENTRYPOINT ["/opt/relax-web/start.sh"] —— 容器启动时执行 start.sh。不用 CMD 而用 ENTRYPOINT,是因为我希望这个脚本一定会被执行,不会被 docker run 后面的命令覆盖掉。
start.sh —— Java 怎么启动
启动脚本是整个部署里最灵活的部分。它负责把环境变量翻译成 Java 启动参数,并且处理日志路径这些细节。
#!/bin/sh
# ============================================================
# ⚠️ 新项目部署时需修改的配置项(已用 [MODIFY] 标记)
# ============================================================
# [MODIFY] JVM 内存参数,根据项目实际需求调整
if [ -z "$JAVA_OPTS" ]; then
JAVA_OPTS="-Xms128m -Xmx128m"
fi
# [MODIFY] JAR 包所在目录,默认为脚本同级目录
if [ -z "$JAR_PATH" ]; then
JAR_PATH="$(cd "$(dirname "$0")" && pwd)"
fi
# [MODIFY] Spring 激活环境:prod / test / dev
if [ -z "$EXAM_ENV" ]; then
EXAM_ENV="prod"
fi
# [MODIFY] 是否启用文件日志,设为 false 可禁用
if [ "x$LOG" != "xfalse" ]; then
# 日志目录 = JAR_PATH/logs(动态计算,非硬编码)
LOG_DIR="$JAR_PATH/logs"
mkdir -p "$LOG_DIR"
LOGGING_OPT="-Dlogging.path=$LOG_DIR"
else
LOGGING_OPT=""
fi
echo "Starting with: $JAVA_OPTS $LOGGING_OPT -DSpring.profiles.active=$EXAM_ENV -jar $JAR_PATH/*.jar"
java $JAVA_OPTS $LOGGING_OPT -DSpring.profiles.active=$EXAM_ENV -Dspring.config.additional-location=file:/opt/relax-web/config/,file:/opt/relax-web/conf/ -jar "$JAR_PATH"/*.jar这个脚本的设计思路是 「每个参数都有默认值,但每个参数都可以通过环境变量覆盖」:
JAVA_OPTS:不传就用默认的128m,传了就用你传的。在.env文件里写JAVA_OPTS=-Xms256m -Xmx256m就能覆盖JAR_PATH:默认取脚本自身所在目录。用$(cd "$(dirname "$0")" && pwd)获取绝对路径,不管从哪个目录启动容器都不会出错EXAM_ENV:Spring 的激活环境,默认prodLOG:设为false可以禁用文件日志(只输出到控制台)。默认是开启的
为什么用 if [ -z "$VAR" ] 而不是 ${VAR:-default}
两种写法效果一样,但 if 写法的可读性更好,尤其是新手看脚本的时候。部署脚本不是炫技的地方,清晰第一。
最后一行 java 命令里有一个关键参数:
-Dspring.config.additional-location=file:/opt/relax-web/config/,file:/opt/relax-web/conf/这个参数告诉 Spring Boot:除了 jar 包内的配置文件,还要额外从容器内的 /opt/relax-web/conf/ 目录读取配置。这个目录通过 Docker volume 挂载了宿主机的配置文件,后面讲 deploy.conf 的时候你会看到。
这意味着:修改配置不用重新构建镜像,改完配置文件重启容器就行。这在生产环境非常实用。
deploy.conf —— 容器怎么跑
deploy.conf 存放的是额外的 docker run 参数,一行一个:
-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逐行解读:
| 参数 | 作用 |
|---|---|
-p 28031:8031 | 端口映射:宿主机 28031 → 容器内 8031。外面套一层 Nginx 做反向代理,不直接暴露容器端口 |
-v .../upload:/opt/relax-web/upload | 文件上传目录挂载到宿主机 /data/ 下,容器重建文件不丢 |
-v .../config:/opt/relax-web/config | 配置目录挂载,和 start.sh 里的 additional-location 对应 |
--network dualai | 加入名为 dualai 的 Docker 网络,方便和其他容器(如 PostgreSQL)通过容器名互访 |
-v .../conf:/opt/relax-web/conf | Spring 配置文件目录挂载,实现「改配置不重建镜像」 |
为什么端口映射是 28031 而不是直接 8031
个人习惯:给每个服务的宿主机端口加一个前缀(这里是 2),避免和开发环境的端口冲突。容器内还是用原始的 8031,代码里不用改任何东西。Nginx 反向代理到 127.0.0.1:28031 即可。
关于 Docker 网络
docker network create dualai 创建好之后,所有加入这个网络的容器可以通过容器名互访。比如 PostgreSQL 容器叫 pgvector,Java 应用的数据库连接串就可以写 jdbc:postgresql://pgvector/relax_admin,不需要记 IP 地址。
.env —— 环境变量(可选)
.env 文件用来覆盖 Dockerfile 里的默认环境变量,格式是 KEY=VALUE:
# .env 示例
JAVA_OPTS=-Xms256m -Xmx256m
DASHSCOPE_API_KEY=sk-xxxxxx
DB_URL=jdbc:postgresql://pgvector/relax_admin
DB_USERNAME=postgres
DB_PASSWORD=your_password
MAIL_USERNAME=your_email@yeah.net
MAIL_PASSWORD=your_auth_code安全提醒
.env 文件里可能有 API Key、数据库密码等敏感信息,绝对不能提交到 Git 仓库。在服务器上直接创建这个文件,通过 .gitignore 排除掉。
配置管理:三套配置各司其职
Spring Boot 的配置文件管理是我特别想讲清楚的部分,因为很多人在这块踩过坑。
我的做法是三套配置文件分层管理:
优先级从高到低:
环境变量(最高)
↓ 覆盖
额外挂载的配置文件(conf/)
↓ 覆盖
jar 包内的 application.yml(最低)application.yml —— 代码里的默认配置
这是打进 jar 包里的主配置文件,定义了所有通用配置和合理的默认值。敏感信息(密码、API Key)全部用 ${ENV_VAR} 占位:
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME:}
password: ${DB_PASSWORD:}
ai:
openai:
api-key: ${DASHSCOPE_API_KEY}铁律
密码、API Key、授权码这类敏感信息,永远不要写死在配置文件里,更不要提交到 Git。用环境变量或者额外配置文件来管理。
conf/application-prod.yml —— 生产环境专属配置
这个文件通过 volume 挂载到容器内,放的是生产环境特有的配置(比如 AI 模型参数、Sa-Token 配置等):
spring:
config:
import: optional:classpath:config/dualai-config-prod.properties
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: kimi-k2.6
temperature: 0.7
sa-token:
token-name: Relax-token
timeout: 2592000
is-concurrent: false
token-style: simple-uuid
is-print: false它通过 spring.config.import 又引入了 dualai-config-prod.properties——这是真正存放数据库密码和邮箱授权码的地方。
conf/dualai-config-prod.properties —— 敏感配置
# ---------- PostgreSQL 数据库 ----------
spring.datasource.url=jdbc:postgresql://pgvector/relax_admin
spring.datasource.username=postgres
spring.datasource.password=********
# ---------- 邮箱 SMTP ----------
relax.mail.from=dualai@yeah.net
relax.mail.auth-code=********注意
这个文件不能提交到 Git(已在 .gitignore 中排除),只在服务器上存放。它的值会覆盖 application.yml 里的占位符。
配置分层的完整链路
用一张图把三层配置的关系理清楚:
jar 包内(代码自带)
├── application.yml ← 通用配置 + 占位符 ${DB_URL}
│
volume 挂载(服务器上改)
├── conf/application-prod.yml ← 生产环境覆盖项
│ └── import dualai-config-prod.properties ← 敏感配置
│
环境变量(最高优先级)
├── DASHSCOPE_API_KEY=sk-xxx
├── DB_URL=...
└── ...这样做的好处:
- 代码里的配置可以放心提交 Git —— 里面没有敏感信息
- 生产配置改起来方便 —— 改完挂载的配置文件,
docker restart就行,不用重新构建镜像 - 本地开发和生产完全隔离 —— 本地用
application-local.properties(也是.gitignore排除的),服务器用dualai-config-prod.properties
前端部署:简单到令人发指
相比后端的 Docker 容器化,前端部署简单到不需要什么技术含量——把构建产物丢到 Nginx 目录下就完事了。
前端项目(比如 bookmark_box)在本地 npm run build 之后会产出 dist/ 目录,里面全是 HTML、CSS、JS 和静态资源。部署时把这个 dist/ 目录的内容复制到 Nginx 的 html 根目录下即可:
# 前端部署的本质就是这一步
cp -r dist/* /opt/docker/nginx/html/bookmark_box/Nginx 配置中为每个前端项目配一个 location 块:
server {
listen 80;
server_name your-domain.com;
# 前端项目:bookmark_box
location /bookmark/ {
alias /opt/docker/nginx/html/bookmark_box/;
try_files $uri $uri/ /bookmark/index.html;
}
# 后端 API 反向代理
location /api/ {
proxy_pass http://127.0.0.1:28031/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}try_files 的作用
try_files $uri $uri/ /bookmark/index.html; 这行是 Vue/React 等 SPA 应用的标配——用户直接访问子路由时,Nginx 找不到对应的文件,就回退到 index.html,让前端路由接管。
一次完整部署的操作流程
把所有步骤串起来,一次完整的部署长这样:
后端部署步骤
# 1. 本地打包(在你的开发机上)
mvn clean package -DskipTests
# 2. 传输到服务器(Cloud Toolkit 一键完成,或手动 SCP)
scp target/relax-web-*.jar user@your-server:/opt/docker/projects/relax-web/
scp Dockerfile start.sh user@your-server:/opt/docker/projects/relax-web/
# 3. SSH 到服务器执行部署脚本
ssh user@your-server
bash /opt/docker/deploy.sh如果用的是 Cloud Toolkit,步骤 1-3 可以合并为 IDE 里的一次点击操作。
前端部署步骤
# 1. 本地构建
npm run build
# 2. 传输到服务器
scp -r dist/ user@your-server:/opt/docker/projects/bookmark_box/
# 3. SSH 到服务器执行部署脚本
ssh user@your-server
bash /opt/docker/deploy.sh关于部署脚本
deploy.sh 会自动识别项目类型(前端还是后端),分别执行对应的部署逻辑。它的具体实现在下一篇文章 统一部署脚本:一个 Shell 搞定前后端自动化部署 里详细讲。
部署后的验证
部署完成之后,快速验证:
# 查看容器状态
docker ps --filter name=relax-web
# 查看容器日志(最后 50 行)
docker logs --tail 50 relax-web
# 查看前端是否就位
ls -la /opt/docker/nginx/html/bookmark_box/
# 测试后端接口
curl http://127.0.0.1:28031/api/health📷 图片位(待补充)
应放什么图:
docker ps输出截图,展示容器正常运行状态docker logs输出截图,展示应用启动成功的日志- 浏览器访问前端页面的截图,展示部署成功后的效果
常见坑与排查
部署过程中最容易遇到的问题,提前列一下:
容器启动后立即退出
# 查看退出原因
docker logs relax-web
# 常见原因:
# 1. JVM 内存参数过大,超出服务器可用内存 → 减小 JAVA_OPTS
# 2. 数据库连不上 → 检查 --network dualai 和数据库容器是否在同一网络
# 3. 配置文件缺失 → 检查 conf/ 目录的 volume 挂载是否正确端口冲突
# 查看端口占用
ss -tlnp | grep 28031
# 如果旧容器没清干净
docker stop relax-web && docker rm relax-web配置不生效
# 进入容器检查配置文件是否挂载成功
docker exec -it relax-web cat /opt/relax-web/conf/application-prod.yml
# 如果文件为空或不存在,检查 deploy.conf 中的 volume 路径前端页面 404
# 检查 Nginx 配置语法
docker exec -it nginx nginx -t
# 检查文件是否到位
ls -la /opt/docker/nginx/html/bookmark_box/
# 重载 Nginx 配置(如果改了 nginx.conf)
docker exec -it nginx nginx -s reload小结
这篇讲了部署的「静态结构」——每个文件是什么、干什么、为什么这么设计。核心要点回顾:
- 后端四个文件搞定:
Dockerfile定义镜像、start.sh控制启动、deploy.conf配置容器参数、.env覆盖环境变量 - 配置三层分离:代码内默认值 → 挂载配置文件 → 环境变量,敏感信息绝不入 Git
- 前端极简部署:
dist/丢到 Nginx 目录,配好反向代理 - 统一脚本一键部署:不管是前端还是后端,都通过同一个
deploy.sh处理
下一篇 统一部署脚本:一个 Shell 搞定前后端自动化部署 会把 deploy.sh 这个脚本逐行拆解——它是怎么自动识别项目类型的、前端和后端的部署逻辑分别是什么、以及怎么扩展新项目。