Docker 部署 SpringBoot:从镜像构建到服务器运行
前三篇分别介绍了 Docker 的核心概念、Dockerfile 构建和 Compose 编排,到这里,我们已经能在本地把一个 Spring Boot 应用连同 MySQL、Redis 一起运行起来。
但本地跑通不等于完成部署。服务器上没有你刚刚构建的镜像,生产环境不能继续使用开发时的数据库密码,服务器的内存和网络配置也与本地不同。直接把本地的 Compose 文件复制过去,应用可能启动失败,也可能容器看起来正常,实际却无法连接数据库、读取配置或对外提供服务。
这篇把这些步骤串起来:从本地构建并验证镜像,到把镜像交付到服务器,最后通过 Compose 启动整套服务。
目录
- 完整部署流程
- [本地构建 SpringBoot 镜像](#本地构建 SpringBoot 镜像)
- 本地运行验证
- [使用 Compose 编排应用和依赖](#使用 Compose 编排应用和依赖)
- 把镜像交付到服务器
- 服务器启动应用
- 开发环境和生产环境的区别
- 常见部署问题
- 小结
完整部署流程
从本地代码到服务器运行,核心步骤如下:

本地构建 SpringBoot 镜像
假设你已经有一个可以运行的 SpringBoot 项目,先写 Dockerfile:
dockerfile
# ---------- 构建阶段 ----------
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
# 先复制 pom.xml,利用构建缓存
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B && \
mv target/*.jar target/app.jar
# ---------- 运行阶段 ----------
FROM eclipse-temurin:17-jre
WORKDIR /app
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
COPY --from=builder /build/target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
多阶段构建把编译和运行分开。构建阶段包含完整的 Maven 环境,运行阶段只保留 JRE,避免把源码、Maven 依赖和构建工具带入最终镜像。
注意 mv target/*.jar target/app.jar 这一步。SpringBoot 打包后 target 目录下可能有多个 jar(比如 app.jar 和 app-sources.jar),用 *.jar 直接 COPY 可能匹配到不需要的文件。先重命名再 COPY,更严谨。
构建镜像:
bash
docker build -t my-app:1.0 .
本地运行验证
在推送到服务器之前,先在本地确认镜像能正常运行:
bash
docker run -d --name my-app -p 8080:8080 my-app:1.0
验证方式:
bash
# 检查容器是否在运行
docker ps
# 查看启动日志,确认没有异常
docker logs my-app
# 测试接口
curl http://localhost:8080/actuator/health
如果这一步就出问题,先在本地解决。不要把没验证过的镜像传到服务器------在服务器上排查问题比本地慢得多。
确认没问题后,清理测试容器:
bash
docker stop my-app && docker rm my-app
使用 Compose 编排应用和依赖
单个应用容器只是整个服务的一部分。一个典型的 SpringBoot 项目还需要 MySQL 和 Redis。用 Compose 把它们编排在一起:
yaml
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: myapp
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7
command: redis-server --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 5s
timeout: 3s
retries: 5
my-app:
image: my-app:1.0
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/myapp
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
SPRING_REDIS_HOST: redis
SPRING_REDIS_PASSWORD: ${REDIS_PASSWORD}
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
mysql-data:
几个细节:
端口只映射需要外部访问的服务。 my-app 的 8080 端口映射了,因为你要从浏览器或 curl 访问。MySQL 和 Redis 没有映射端口------它们只需要被 my-app 访问,不需要从宿主机或外部网络连进来。Compose 自动创建的网络中,容器之间用服务名互通,不需要暴露端口。
如果开发时需要用 Navicat 连 MySQL 调试,可以在本地单独加 ports: - "3306:3306",但不要把这个配置带到服务器上。
Redis 也加了健康检查。 depends_on 只保证容器启动顺序,不保证服务就绪。Redis 启动虽然快,但加上健康检查后,condition: service_healthy 能确保 Redis 真正可以接受连接后才启动 my-app,和 MySQL 的处理方式保持一致。
密码用变量引用,不要硬编码。 创建一个 .env 文件存放实际值:
text
MYSQL_ROOT_PASSWORD=your-strong-password
REDIS_PASSWORD=your-redis-password
这个文件不要提交到版本控制。在 .gitignore 中加上 .env。
本地验证 Compose 编排:
bash
docker compose up -d
docker compose ps # 确认三个服务都正常运行
docker compose logs -f my-app # 跟踪应用日志
三个服务全部正常后,进入下一步。
把镜像交付到服务器
镜像在本地构建好了,下一步是传到服务器
个人项目或小规模部署,用文件传输:
bash
# 本地导出镜像
docker save my-app:1.0 -o my-app.tar
# 传到服务器
scp my-app.tar user@server:/tmp/
# 服务器上导入
ssh user@server "docker load -i /tmp/my-app.tar"
docker save 把镜像打包成 tar 文件,docker load 在服务器上还原。简单直接,不需要额外搭建 Registry。
团队项目推荐用镜像仓库:
bash
# 登录仓库
docker login my-harbor.com
# 打标签
docker tag my-app:1.0 my-harbor.com/backend/my-app:1.0
# 推送
docker push my-harbor.com/backend/my-app:1.0
服务器上拉取:
bash
docker login my-harbor.com
docker pull my-harbor.com/backend/my-app:1.0
镜像仓库的好处是版本管理和多人协作。一个人部署用 save/load 没问题,多个人部署同一个服务时,Registry 会省很多事。
服务器启动应用
服务器上需要安装 Docker 和 Docker Compose。安装过程不展开了,Docker 官方文档针对各个 Linux 发行版都有详细说明。
把以下文件传到服务器:
text
/opt/my-app/
├── docker-compose.yml
├── .env
启动服务:
bash
cd /opt/my-app
docker compose up -d
确认服务正常:
bash
# 查看容器状态
docker compose ps
# 检查应用日志
docker compose logs -f my-app
# 从外部测试接口
curl http://服务器IP:8080/actuator/health
后续更新应用,如果是用镜像仓库:
bash
docker compose pull # 拉取新版本镜像
docker compose up -d # Compose 自动检测变化并重建容器
如果是用文件传输:
bash
# 本地重新构建并导出
docker build -t my-app:2.0 .
docker save my-app:2.0 -o my-app.tar
scp my-app.tar user@server:/tmp/
# 服务器上导入并更新
ssh user@server "docker load -i /tmp/my-app.tar"
ssh user@server "cd /opt/my-app && docker compose up -d"
开发环境和生产环境的区别
本地开发用的 docker-compose.yml 不能原封不动搬到服务器。几个关键差异:
配置隔离
开发环境和生产环境的数据库密码、Redis 配置通常不同。用 .env 文件分离:
bash
# 开发环境 .env.dev
MYSQL_ROOT_PASSWORD=dev123
REDIS_PASSWORD=devredis
# 生产环境 .env.prod
MYSQL_ROOT_PASSWORD=<强密码>
REDIS_PASSWORD=<强密码>
启动时指定环境文件:
bash
docker compose --env-file .env.prod up -d
敏感信息不要提交到版本控制。
数据持久化
Compose 中定义的 volume(比如 mysql-data)在 docker compose down 后仍然保留。但如果执行 docker compose down -v,volume 会被删除,数据库数据就没了。
生产环境操作时要特别注意这一点。数据库备份应该独立于 Docker 的 volume 管理。
JVM 内存限制
开发环境笔记本有 16GB 内存,随便跑。服务器上如果只有 2GB 内存,三个服务一起跑可能直接 OOM。
容器环境下,JVM 需要感知内存限制。在 Dockerfile 的 ENTRYPOINT 中加入参数:
dockerfile
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
然后在 Compose 中配置:
yaml
my-app:
environment:
JAVA_OPTS: "-XX:MaxRAMPercentage=75.0 -XX:+UseContainerSupport"
deploy:
resources:
limits:
memory: 512M
-XX:MaxRAMPercentage=75.0 让 JVM 按容器内存限制的 75% 分配堆内存,留一部分给非堆区域。-XX:+UseContainerSupport 在 JDK 10+ 默认开启,确保 JVM 能正确识别容器的内存限制。
注意 ENTRYPOINT 的写法。如果用 ENTRYPOINT ["java", "-jar", "app.jar"] 这种 exec 形式,JVM 不会读取 JAVA_OPTS 环境变量。必须用 shell 形式 sh -c 让 shell 展开变量。
日志
容器内的应用应该把日志输出到 stdout/stderr,而不是写入容器内的文件。SpringBoot 默认这样做,但如果你自定义了 logback 配置写入文件,需要改回来。
日志输出到 stdout 后,用 docker logs 查看:
bash
docker compose logs -f my-app
Docker 默认会保留日志文件,但不会自动清理。可以在 Compose 中限制日志大小:
yaml
my-app:
logging:
options:
max-size: "10m"
max-file: "3"
每个容器最多保留 3 个 10MB 的日志文件,超出后自动轮转。
常见部署问题
部署失败时,按这个顺序排查:
1. 容器有没有启动
bash
docker compose ps
如果容器状态是 Exited 或者根本没出现,看日志:
bash
docker compose logs my-app
常见原因:镜像不存在、Dockerfile 语法错误、ENTRYPOINT 指定的命令不存在。
2. 服务之间能不能通信
容器启动了但应用报连接错误,检查网络配置:
text
错误写法:jdbc:mysql://localhost:3306/myapp
正确写法:jdbc:mysql://mysql:3306/myapp
容器内的 localhost 指向容器自己,不是宿主机,也不是其他容器。Compose 网络中用服务名访问其他服务。
3. 配置是否正确
数据库连接失败,检查顺序:
- 依赖服务的健康检查是否通过(
docker compose ps看 STATUS 列) - 连接地址写的是服务名还是 localhost
- 密码是否和
.env中的一致 - 环境变量名是否和应用配置中的对应(比如
SPRING_DATASOURCE_PASSWORD)
4. 时区问题
容器默认使用 UTC 时区。如果数据库存储的时间比实际时间早 8 小时,在 Compose 中设置时区:
yaml
my-app:
environment:
TZ: Asia/Shanghai
小结
部署链路:写 Dockerfile → 本地构建验证 → Compose 编排 → 镜像交付到服务器 → 启动服务。
Docker 解决的是环境一致性的问题,本地能跑的镜像,服务器上大概率也能跑。但服务器部署还有配置隔离、数据持久化、资源限制这些事要处理,这些不能靠容器本身解决,需要在 Compose 配置中逐一处理。
部署 checklist:
- Dockerfile 写好了,多阶段构建,jar 文件名确定
- 镜像本地构建成功,
docker run能正常启动 - Compose 编排本地跑通,所有服务能互相访问
- 镜像交付到服务器(Registry 或 save/load)
- 服务器安装了 Docker 和 Docker Compose
- 生产配置单独准备(.env),密码没有硬编码
- JVM 内存参数已设置,ENTRYPOINT 能读取 JAVA_OPTS
docker compose up -d,服务正常运行