Docker 部署 SpringBoot:从镜像构建到服务器运行

Docker 部署 SpringBoot:从镜像构建到服务器运行

前三篇分别介绍了 Docker 的核心概念、Dockerfile 构建和 Compose 编排,到这里,我们已经能在本地把一个 Spring Boot 应用连同 MySQL、Redis 一起运行起来。

但本地跑通不等于完成部署。服务器上没有你刚刚构建的镜像,生产环境不能继续使用开发时的数据库密码,服务器的内存和网络配置也与本地不同。直接把本地的 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.jarapp-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. 配置是否正确

数据库连接失败,检查顺序:

  1. 依赖服务的健康检查是否通过(docker compose ps 看 STATUS 列)
  2. 连接地址写的是服务名还是 localhost
  3. 密码是否和 .env 中的一致
  4. 环境变量名是否和应用配置中的对应(比如 SPRING_DATASOURCE_PASSWORD

4. 时区问题

容器默认使用 UTC 时区。如果数据库存储的时间比实际时间早 8 小时,在 Compose 中设置时区:

yaml 复制代码
my-app:
  environment:
    TZ: Asia/Shanghai

小结

部署链路:写 Dockerfile → 本地构建验证 → Compose 编排 → 镜像交付到服务器 → 启动服务。

Docker 解决的是环境一致性的问题,本地能跑的镜像,服务器上大概率也能跑。但服务器部署还有配置隔离、数据持久化、资源限制这些事要处理,这些不能靠容器本身解决,需要在 Compose 配置中逐一处理。


部署 checklist:

  1. Dockerfile 写好了,多阶段构建,jar 文件名确定
  2. 镜像本地构建成功,docker run 能正常启动
  3. Compose 编排本地跑通,所有服务能互相访问
  4. 镜像交付到服务器(Registry 或 save/load)
  5. 服务器安装了 Docker 和 Docker Compose
  6. 生产配置单独准备(.env),密码没有硬编码
  7. JVM 内存参数已设置,ENTRYPOINT 能读取 JAVA_OPTS
  8. docker compose up -d,服务正常运行
相关推荐
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 15 章 生产级特性 使用监控体系 阅读笔记
spring boot·笔记·后端
Eloudy1 小时前
国内加速 docker pull 下载 docker images,特别是 nvidia 的cuda image
docker·gpu·rdma
倔强的石头1061 小时前
Spring Boot 接入金仓数据库:配置分层、启动自检与常见错误
数据库·spring boot·后端
Chief_fly1 小时前
ARM 麒麟系统服务器构建docker镜像
运维·服务器·docker
Dawn-bit1 小时前
Linux文本处理三剑客之sed详解
linux·运维·服务器·云计算·运维开发
czhaii2 小时前
汇川注塑机伺服驱动器故障维修排除方法
运维·服务器
Cx330❀2 小时前
【Linux网络】深入 HTTP 协议(五):从 Cookie/Session 原理到 C++ 源码实战
linux·运维·服务器·开发语言·网络·c++·http
神秘的MT2 小时前
C# WinForm Socket 类 常用方法 (C# .NET,TCP 场景)
服务器·网络·学习·tcp/ip·c#·winform
凤山老林2 小时前
SpringBoot + Configuration2 实现配置的实时双向更新
java·spring boot·后端