Java jar包Docker部署实战:多阶段构建与镜像优化指南

1. 为什么把 jar 包塞进 Docker 是个值得聊的话题

刚入行那会儿,我部署一个 Java 服务的标准动作是这样的:本地mvn package打出 jar,scp 传到服务器,写个nohup java -jar xxx.jar &,然后祈祷它别在半夜挂掉。挂了就挂了,第二天上班看日志,发现是环境变量没配、JDK 版本不对、或者某个依赖的 so 库缺失。这种“人肉运维”的日子,相信不少同行都经历过。

后来容器化浪潮来了,大家都说“把 jar 包做成镜像,一键跑起来”。听起来很美,但真动手的时候,问题一个接一个:基础镜像选哪个?JDK 版本怎么对齐?时区怎么设?日志往哪写?配置文件是打进镜像还是挂载出来?镜像打出来 800MB,推送到仓库要等半天,这合理吗?

这篇内容就是围绕jar 包 Docker 服务器部署这件事,把我这些年踩过的坑、试过的方案、以及最终沉淀下来的一套可复用的流程,完整地摊开来讲。不管你是刚接触容器化部署的后端开发,还是需要自己维护服务运行环境的全栈选手,或者是想把手头那堆“裸跑”的 Java 服务收编进容器的运维同学,这里面的内容都能直接拿去用。

核心关键词就几个:jar 包、Docker 镜像、多阶段构建、容器部署、日志挂载、时区配置、健康检查。我会从方案选型的逻辑讲起,再到 Dockerfile 的逐行拆解,然后是完整的构建与部署实操,最后是问题排查的速查表。每一步都会说清楚“为什么这么做”,而不是只给一堆命令让你抄。

2. 方案选型:别急着写 Dockerfile,先把这几件事想清楚

2.1 基础镜像怎么选:JRE 还是 JDK,alpine 还是 slim

这是第一个岔路口。很多人上来就FROM openjdk:8-jdk,能用是能用,但镜像体积直接飙到 600MB 以上。对于一个只需要跑 jar 包的生产环境来说,编译工具链、源码、文档这些东西全是累赘。

我的选择逻辑是这样的:

镜像类型体积量级适用场景注意事项
openjdk:8-jdk600MB+需要在容器内编译代码生产环境跑 jar 包属于杀鸡用牛刀
openjdk:8-jre300MB+纯运行环境比 JDK 小一半,但仍有优化空间
openjdk:8-jre-slim200MB 左右生产环境首选去掉了文档和部分非必要组件
openjdk:8-jre-alpine100MB 以内追求极致体积基于 musl libc,部分 native 依赖可能不兼容
eclipse-temurin:8-jre200MB 左右替代 Oracle JDK社区维护,更新及时

提示:alpine 镜像虽小,但如果你的项目依赖了某些基于 glibc 的 native 库(比如某些加密库、图像处理库),跑起来会报No such file or directory或者Error loading shared library。这种情况要么换 slim 镜像,要么在 alpine 里补装libc6-compat,但补装之后体积优势就没那么明显了。

我个人的习惯是:优先用eclipse-temurin的 jre-slim 版本。它是社区维护的 OpenJDK 构建,更新频率有保障,体积也控制得不错。如果项目对 JDK 版本有特殊要求(比如必须用某个特定发行版),再按需调整。

2.2 构建方式:单阶段还是多阶段

单阶段构建就是在一个 Dockerfile 里从头写到尾:拉基础镜像、拷贝 jar、设置启动命令。简单直接,但有个问题——如果你需要在构建过程中做一些额外操作(比如解压 jar、分层处理、生成配置文件),这些操作产生的中间文件都会留在最终镜像里。

多阶段构建则是把“构建”和“运行”分开。构建阶段可以用一个带完整工具链的镜像,运行阶段只拷贝最终产物。对于 jar 包部署来说,多阶段构建的典型用法是:

# 构建阶段:解压 jar,做分层处理 FROM eclipse-temurin:8-jre AS builder WORKDIR /build COPY app.jar app.jar RUN java -Djarmode=layertools -jar app.jar extract # 运行阶段:只拷贝分层后的文件 FROM eclipse-temurin:8-jre WORKDIR /app COPY --from=builder /build/dependencies/ ./ COPY --from=builder /build/spring-boot-loader/ ./ COPY --from=builder /build/snapshot-dependencies/ ./ COPY --from=builder /build/application/ ./ ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

这种分层构建的好处是:当你的业务代码变更时,只有application层会变,dependencies层可以复用缓存。推送到镜像仓库时,也只推送变更的层,大大加快了部署速度。实测下来,一个 200MB 的镜像,分层之后日常更新只需要推送几 MB 的增量。

注意:分层构建依赖 Spring Boot 的layertools,如果你的项目不是 Spring Boot 或者版本较老,这个方案不适用。普通项目用单阶段构建就够了,别为了分层而分层。

2.3 配置管理:打进镜像还是外部挂载

这个问题争论了很久。我的观点很明确:配置绝对不能打进镜像。

原因很简单——镜像应该是不可变的构建产物,同一份镜像在测试环境、预发环境、生产环境都应该能跑。如果把数据库连接串、Redis 地址这些写死在镜像里,那每次换环境都要重新构建,镜像的“一次构建、到处运行”就成了一句空话。

正确的做法是:

  • 镜像里只放一份默认配置(或者干脆不放),作为兜底
  • 实际配置通过环境变量注入,或者挂载外部配置文件
  • 敏感信息(密码、密钥)通过环境变量或者配置中心下发,绝不写进 Dockerfile
# 通过环境变量覆盖配置 docker run -d \ -e SPRING_PROFILES_ACTIVE=prod \ -e DB_HOST=10.0.0.1 \ -e DB_PASSWORD=xxxx \ my-java-app:1.0.0

如果配置项特别多,用环境变量会显得很臃肿,这时候可以挂载一个外部配置文件:

docker run -d \ -v /opt/config/application-prod.yml:/app/config/application-prod.yml \ my-java-app:1.0.0

然后在启动命令里指定配置路径:

ENTRYPOINT ["java", "-jar", "app.jar", "--spring.config.location=/app/config/"]

2.4 日志输出:别让日志烂在容器里

容器里的日志如果只往文件写,会有两个问题:一是容器重启后日志丢失,二是要看日志得先docker exec进去,非常不方便。

我的做法是:让 Java 应用把日志输出到标准输出(stdout)和标准错误(stderr),然后由 Docker 的日志驱动统一收集。这样用docker logs就能直接看,也可以配置日志驱动转发到集中式日志系统。

在 Spring Boot 里,只需要在application.yml里配置:

logging: file: name: # 留空,不写文件 pattern: console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

如果有些日志必须落盘(比如审计日志),那就挂载一个数据卷出来:

docker run -d \ -v /opt/logs/my-app:/app/logs \ my-java-app:1.0.0

实操心得:挂载日志目录时,宿主机目录的权限要和容器内运行用户的 UID 匹配。我遇到过好几次容器启动失败,就是因为宿主机目录是 root 创建的,容器内用非 root 用户跑,写不进去。解决办法是在 Dockerfile 里创建用户时指定 UID,然后宿主机目录也 chown 成同样的 UID。

3. Dockerfile 逐行拆解:一份可以直接抄的模板

3.1 完整 Dockerfile 示例

先上模板,再逐段解释。这是一个经过多个生产项目验证的 Dockerfile,适用于大多数 Spring Boot 项目的 jar 包部署:

# 基础镜像 FROM eclipse-temurin:8-jre-jammy # 维护者信息(可选) LABEL maintainer="dev-team" # 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # 创建非 root 用户 RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser # 设置工作目录 WORKDIR /app # 拷贝 jar 包 COPY target/my-app-1.0.0.jar app.jar # 创建日志目录并授权 RUN mkdir -p /app/logs && chown -R appuser:appuser /app # 切换用户 USER appuser # 暴露端口(根据实际项目调整) EXPOSE 8080 # JVM 参数 ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200" # 启动命令 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar app.jar"]

3.2 时区配置:为什么必须显式设置

基础镜像默认用的是 UTC 时间。如果你的应用日志、数据库记录、定时任务都依赖本地时间,不设时区就会出现“日志时间比实际时间少 8 小时”的经典问题。

设置时区的正确姿势是两行:

ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

第一行设置环境变量,第二行创建软链接并写入时区文件。有些教程只写第一行,在某些基础镜像里不生效,所以两行都写上最稳妥。

注意:如果你用的是 alpine 镜像,/usr/share/zoneinfo路径可能不存在,需要先apk add --no-cache tzdata安装时区数据。

3.3 非 root 用户运行:安全底线

容器默认以 root 用户运行,这在生产环境是个安全隐患。一旦容器被突破,攻击者就拥有了 root 权限。虽然容器本身有一定的隔离性,但多一层防护总归是好的。

创建非 root 用户的步骤:

RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
  • -r表示创建系统用户
  • -g指定用户组
  • -d指定家目录
  • -s /sbin/nologin禁止登录 shell

然后切换用户:

USER appuser

实操心得:切换用户之后,之前用 root 创建的文件和目录,appuser 可能没有写权限。所以要在USER appuser之前,把需要写的目录 chown 给 appuser。我一般会在拷贝完 jar 包之后,统一执行一次chown -R appuser:appuser /app。

3.4 JVM 参数:容器里跑 Java 的特殊考量

在容器里跑 Java,JVM 参数有几个坑:

堆内存设置。以前大家习惯-Xms512m -Xmx1024m写死,但在容器环境下,如果容器内存限制是 1GB,JVM 堆设了 1GB,加上元空间、线程栈、直接内存,很容易触发 OOM Killer。更合理的做法是用-XX:MaxRAMPercentage:

ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"

这样 JVM 会根据容器内存限制自动计算堆大小,容器给 1GB 就设 750MB 堆,给 2GB 就设 1.5GB 堆,不用改 Dockerfile。

随机数生成。-Djava.security.egd=file:/dev/./urandom这个参数是为了加快随机数生成速度。默认的/dev/random在熵不足时会阻塞,导致应用启动慢。用/dev/urandom或者/dev/./urandom可以避免这个问题。

GC 选择。G1 在大多数场景下表现均衡,-XX:+UseG1GC -XX:MaxGCPauseMillis=200是常见的组合。如果对吞吐量要求更高,可以考虑 Parallel GC;如果对延迟极其敏感,可以考虑 ZGC(需要 JDK 11+)。

3.5 启动命令:ENTRYPOINT 还是 CMD

这两个指令都能指定启动命令,区别在于:

  • ENTRYPOINT:容器启动时固定执行的命令,docker run后面的参数会作为它的附加参数
  • CMD:默认命令,如果docker run指定了命令,CMD 会被覆盖

对于 jar 包部署,我推荐用ENTRYPOINT,并且用sh -c的形式,这样可以解析环境变量:

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

如果写成ENTRYPOINT ["java", "$JAVA_OPTS", "-jar", "app.jar"],$JAVA_OPTS不会被解析,会被当成一个普通的字符串参数传给 java,导致启动失败。这是个很常见的坑。

4. 完整实操:从本地构建到服务器运行

4.1 本地构建镜像

假设项目结构是这样的:

my-project/ ├── src/ ├── target/ │ └── my-app-1.0.0.jar ├── Dockerfile └── pom.xml

构建步骤:

# 1. 先打包 jar mvn clean package -DskipTests # 2. 构建镜像 docker build -t my-java-app:1.0.0 . # 3. 查看镜像体积 docker images | grep my-java-app

如果构建过程中报错COPY failed: no source files were specified,检查一下target目录下 jar 包的名字是否和 Dockerfile 里写的一致。Maven 打包出来的 jar 名字通常包含版本号,比如my-app-1.0.0.jar,别写成my-app.jar。

4.2 镜像体积优化实战

一个未经优化的 Spring Boot 镜像,体积通常在 300MB 以上。经过下面几步优化,可以压到 150MB 左右:

第一步:换基础镜像。从openjdk:8-jdk换成eclipse-temurin:8-jre-jammy,体积从 600MB+ 降到 200MB 左右。

第二步:合并 RUN 指令。每个 RUN 指令都会产生一层,层数越多体积越大。把能合并的合并:

# 不推荐 RUN apt-get update RUN apt-get install -y tzdata RUN rm -rf /var/lib/apt/lists/* # 推荐 RUN apt-get update && \ apt-get install -y --no-install-recommends tzdata && \ rm -rf /var/lib/apt/lists/*

第三步:清理缓存。安装完软件后,把包管理器的缓存删掉。--no-install-recommends也很重要,避免安装一堆用不到的推荐包。

第四步:使用 .dockerignore。在项目根目录创建.dockerignore文件,把不需要打进镜像的东西排除掉:

.git .idea *.iml target/*.jar.original *.md

实测数据:一个典型的 Spring Boot 项目,优化前镜像 380MB,优化后 145MB,推送时间从 3 分钟降到 40 秒。

4.3 服务器上的部署流程

镜像构建好之后,有两种方式放到服务器上:一是推送到镜像仓库,服务器拉取;二是直接导出镜像文件,传到服务器加载。

方式一:镜像仓库

# 本地打标签 docker tag my-java-app:1.0.0 registry.example.com/my-java-app:1.0.0 # 推送 docker push registry.example.com/my-java-app:1.0.0 # 服务器上拉取 docker pull registry.example.com/my-java-app:1.0.0

方式二:导出导入

# 本地导出 docker save my-java-app:1.0.0 -o my-java-app.tar # 传到服务器后导入 docker load -i my-java-app.tar

方式一适合有私有仓库的团队,方式二适合临时部署或者没有仓库的情况。方式二的缺点是每次都要传完整镜像,体积大的时候很慢。

启动容器:

docker run -d \ --name my-java-app \ --restart unless-stopped \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=prod \ -e JAVA_OPTS="-Xms512m -Xmx1024m" \ -v /opt/logs/my-app:/app/logs \ -v /opt/config:/app/config \ --memory 1.5g \ --cpus 1.5 \ my-java-app:1.0.0

参数说明:

  • --restart unless-stopped:容器异常退出时自动重启,除非手动停止
  • -p 8080:8080:端口映射,宿主机端口:容器端口
  • -e:注入环境变量
  • -v:挂载数据卷
  • --memory:内存限制,要比 JVM 堆大一些,留出元空间和系统开销
  • --cpus:CPU 限制

注意:--memory设了 1.5g,JVM 的MaxRAMPercentage设了 75%,那堆最大就是 1.125g。剩下的 375MB 给元空间、线程栈、直接内存、GC 等。这个比例可以根据实际情况调整,如果应用创建大量线程,需要留更多内存给线程栈。

4.4 用 Docker Compose 管理多服务

如果项目还依赖了数据库、Redis 等中间件,用 Docker Compose 会更方便:

version: '3.8' services: app: image: my-java-app:1.0.0 container_name: my-java-app restart: unless-stopped ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod - DB_HOST=mysql - REDIS_HOST=redis - JAVA_OPTS=-Xms512m -Xmx1024m volumes: - ./logs:/app/logs - ./config:/app/config depends_on: - mysql - redis networks: - app-network mysql: image: mysql:8.0 container_name: my-mysql restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD=root123 - MYSQL_DATABASE=mydb volumes: - mysql-data:/var/lib/mysql networks: - app-network redis: image: redis:7-alpine container_name: my-redis restart: unless-stopped volumes: - redis-data:/data networks: - app-network volumes: mysql-data: redis-data: networks: app-network: driver: bridge

启动命令:

docker-compose up -d

查看日志:

docker-compose logs -f app

实操心得:Compose 里的服务名就是容器间通信的主机名。比如 app 服务里配置DB_HOST=mysql,就能直接连到 mysql 容器,不用写 IP。这是 Docker 内置的 DNS 解析,非常方便。

5. 常见问题与排查技巧实录

5.1 容器启动就退出,日志也看不到

这是最常见的问题。容器启动后立刻退出,docker logs显示的信息很少或者为空。

排查思路:

  1. 先看退出码:docker inspect <container_id> | grep ExitCode
  2. 退出码 1:通常是应用启动报错,检查配置文件、数据库连接
  3. 退出码 137:被 OOM Killer 杀了,内存不够,调大--memory或者调小 JVM 堆
  4. 退出码 143:收到了 SIGTERM 信号,正常停止

如果日志为空,可能是应用还没输出日志就挂了。可以临时覆盖启动命令,进容器看看:

docker run -it --entrypoint /bin/bash my-java-app:1.0.0

进去之后手动执行java -jar app.jar,看报什么错。

5.2 时区不对,日志时间差 8 小时

前面说了要设时区,但有时候设了还是不对。检查两个地方:

一是 Dockerfile 里的时区设置是否生效:

docker exec <container_id> date

如果输出的不是北京时间,说明时区没设对。检查/etc/localtime软链接指向哪里。

二是 JVM 的时区。有些 JDK 版本会缓存时区信息,需要在启动参数里显式指定:

ENTRYPOINT ["java", "-Duser.timezone=Asia/Shanghai", "-jar", "app.jar"]

5.3 中文乱码

容器里默认的 locale 通常是 POSIX,不支持中文。如果应用需要处理中文,需要在 Dockerfile 里设置:

ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8

如果基础镜像里没有 C.UTF-8 locale,可以安装:

RUN apt-get update && apt-get install -y locales && \ locale-gen C.UTF-8 && \ rm -rf /var/lib/apt/lists/*

5.4 健康检查配置

Docker 的健康检查可以自动检测容器是否正常运行,配合--restart策略,能在应用假死时自动重启。

HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1

参数说明:

  • --interval=30s:每 30 秒检查一次
  • --timeout=5s:检查超时时间
  • --start-period=60s:容器启动后 60 秒内不检查,给应用启动留时间
  • --retries=3:连续失败 3 次才标记为 unhealthy

注意:健康检查的接口要选轻量的,别选那种会查数据库、查缓存的接口,否则健康检查本身就成了负担。Spring Boot Actuator 的/actuator/health默认只返回应用状态,比较合适。

5.5 常见问题速查表

问题现象可能原因排查命令解决方案
容器启动即退出应用启动报错docker logs <id>检查配置、依赖、端口占用
退出码 137内存不足被 OOMdocker inspect <id>调大内存限制或调小 JVM 堆
日志时间差 8 小时时区未设置docker exec <id> date设置 TZ 和 JVM 时区参数
中文乱码locale 不支持docker exec <id> locale设置 LANG=C.UTF-8
端口无法访问端口未映射或防火墙docker port <id>检查 -p 参数和防火墙规则
配置文件不生效挂载路径错误docker exec <id> ls /app/config检查 -v 参数和文件权限
镜像推送慢镜像层数多、体积大docker history <image>合并 RUN 指令、换基础镜像
容器内无法解析域名DNS 配置问题docker exec <id> cat /etc/resolv.conf指定 --dns 或检查 Docker 网络

5.6 几个容易忽略的细节

文件句柄数限制。容器默认的文件句柄数可能比较小,高并发场景下会报Too many open files。可以在启动时加参数:

docker run --ulimit nofile=65535:65535 ...

优雅停机。Java 应用收到 SIGTERM 信号后,应该先处理完手头的请求再退出。Spring Boot 2.3+ 支持优雅停机:

server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s

然后在docker run时加--stop-timeout 30,给容器 30 秒的停止时间。

镜像标签管理。别只用latest标签,每次构建都用版本号或者 git commit hash 作为标签,方便回滚:

docker build -t my-java-app:$(git rev-parse --short HEAD) .

资源限制要合理。--memory设得太小,应用跑不起来;设得太大,一台服务器上能跑的容器就少了。我的经验值是:JVM 堆大小设为容器内存的 50%-75%,剩下的留给系统。比如容器限制 2GB,JVM 堆设 1GB-1.5GB 比较合适。

6. 进阶玩法:让部署更顺滑的几个技巧

6.1 利用构建缓存加速镜像构建

Docker 构建时会逐层缓存,如果某一层没变,就直接用缓存。所以 Dockerfile 里指令的顺序很重要:变化频率低的放前面,变化频率高的放后面。

# 这些不常变,放前面 FROM eclipse-temurin:8-jre-jammy ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN groupadd -r appuser && useradd -r -g appuser appuser # jar 包经常变,放后面 COPY target/my-app-1.0.0.jar app.jar

这样改代码重新构建时,前面的层都命中缓存,只有 COPY 那一层重新执行,构建速度从几分钟降到几秒。

6.2 多环境配置的优雅管理

不同环境的配置差异,用 Spring Boot 的 profile 机制来管理:

# 测试环境 docker run -e SPRING_PROFILES_ACTIVE=test ... # 生产环境 docker run -e SPRING_PROFILES_ACTIVE=prod ...

然后在 jar 包里放application-test.yml、application-prod.yml,公共配置放application.yml。这样同一份镜像可以在不同环境跑,只需要改一个环境变量。

如果配置项特别敏感,可以用配置中心(比如 Nacos、Apollo),应用启动时从配置中心拉取配置。这样连环境变量都不用改,完全靠配置中心区分环境。

6.3 滚动更新与回滚

用 Docker Compose 或者 Swarm 做滚动更新:

# 构建新版本镜像 docker build -t my-java-app:1.0.1 . # 更新服务 docker-compose up -d --no-deps app

--no-deps表示只更新 app 服务,不动依赖的数据库和 Redis。

回滚就是换回旧版本镜像:

docker-compose stop app docker-compose rm -f app # 修改 docker-compose.yml 里的镜像版本为 1.0.0 docker-compose up -d app

实操心得:每次部署前,先把当前运行的镜像版本记录下来。出问题时能快速回滚。我习惯在服务器上建一个deploy-history.log,每次部署追加一行:时间、版本、操作人。简单但有效。

6.4 监控与告警的接入

容器跑起来只是第一步,还得知道它跑得好不好。几个关键的监控指标:

  • 容器级别:CPU 使用率、内存使用率、网络 IO、磁盘 IO
  • JVM 级别:堆内存使用、GC 次数和耗时、线程数
  • 应用级别:接口响应时间、错误率、QPS

容器级别的监控可以用 cAdvisor + Prometheus + Grafana 这套组合。JVM 级别的监控可以用 Micrometer + Prometheus,Spring Boot Actuator 已经内置了 Micrometer 支持,只需要加依赖和配置:

management: endpoints: web: exposure: include: health,info,metrics,prometheus metrics: export: prometheus: enabled: true

然后在 Prometheus 里配置抓取目标,Grafana 里导入 JVM 监控面板,就能看到堆内存、GC、线程等指标了。

6.5 安全加固的几个建议

除了前面说的非 root 用户运行,还有几个安全措施:

只读文件系统。容器运行时不写文件系统,需要写的地方挂载数据卷:

docker run --read-only --tmpfs /tmp -v /opt/logs:/app/logs ...

禁用特权模式。除非必要,不要用--privileged。

限制能力。默认容器拥有一堆 Linux capabilities,很多用不到,可以丢弃:

docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE ...

镜像扫描。定期扫描镜像里的漏洞:

docker scan my-java-app:1.0.0

或者用 Trivy 等工具:

trivy image my-java-app:1.0.0

这些措施看起来繁琐,但都是生产环境的基本要求。我见过太多团队因为容器安全没做好,被扫描出高危漏洞,临时加班整改。

7. 我踩过的几个印象深刻的坑

说几个真实经历,都是文档里不会写、但实际部署时大概率会遇到的问题。

第一个坑:jar 包名字带版本号,Dockerfile 写死了。项目迭代时版本号从 1.0.0 升到 1.0.1,Dockerfile 里的COPY target/my-app-1.0.0.jar app.jar没改,构建时报文件不存在。后来改成用 ARG 参数:

ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar

这样不管 jar 包叫什么名字,都能匹配到。

第二个坑:容器内存限制和 JVM 堆不匹配。容器限制 1GB,JVM 堆设了 1GB,结果应用跑一会儿就被 OOM Killer 杀了。原因是 JVM 除了堆,还需要元空间、线程栈、直接内存、代码缓存等,这些加起来可能好几百 MB。后来改成-XX:MaxRAMPercentage=70,让 JVM 自己算,问题解决。

第三个坑:日志挂载目录权限不对。宿主机目录是 root 创建的,容器里用 appuser 跑,写不进去,应用启动时报Permission denied。解决办法是在 Dockerfile 里创建用户时指定 UID,宿主机目录也 chown 成同样的 UID:

RUN groupadd -r -g 1000 appuser && useradd -r -u 1000 -g appuser appuser
chown -R 1000:1000 /opt/logs/my-app

第四个坑:健康检查接口太重。一开始健康检查用的是业务接口,结果健康检查本身把数据库连接池占满了。后来换成 Actuator 的/actuator/health,并且配置了management.endpoint.health.show-details=never,只返回 UP 或 DOWN,不查具体组件。

第五个坑:时区设了但没生效。Dockerfile 里写了ENV TZ=Asia/Shanghai,但容器里date命令输出的还是 UTC。原因是基础镜像里没有时区数据文件。后来加了RUN apt-get update && apt-get install -y tzdata,问题解决。所以设时区之前,先确认基础镜像里有没有 tzdata。

这些坑说起来都不复杂,但没踩过就是不知道。希望看到这里的同行能少走点弯路。

8. 写在最后的一些个人习惯

部署这件事,工具和命令都是死的,关键是形成一套自己的流程和检查清单。我现在的习惯是:

每次部署前,先本地构建镜像,跑一遍docker run确认能正常启动。然后检查 Dockerfile 有没有硬编码的配置、有没有用 root 用户、时区设了没有、健康检查配了没有。部署到服务器后,先看日志确认启动成功,再看健康检查状态,最后验证接口是否正常。

这套流程看起来繁琐,但比出了问题再回头排查要省时间得多。尤其是生产环境,一次部署失败可能就是几十分钟的服务不可用,提前多花五分钟检查,能避免很多麻烦。

另外,Dockerfile 和 docker-compose.yml 一定要纳入版本管理,和代码一起提交。我见过有团队把 Dockerfile 放在服务器上手动维护,结果换个人部署就出问题。基础设施即代码,这句话不是口号,是实打实的经验。

后续如果项目规模变大,可以考虑上 Kubernetes,把部署、扩缩容、滚动更新这些事交给编排系统。但那是另一个话题了,对于大多数中小项目来说,Docker + Docker Compose 已经足够。先把这套玩熟,再考虑下一步。

相关新闻

C盘清理与注册表整理全攻略:三款工具组合实操流程

C盘清理与注册表整理全攻略:三款工具组合实操流程

C 盘又变红了&#xff0c;这大概是 Windows 用户最熟悉也最头疼的画面。不管你是新装系统用了一两年&#xff0c;还是刚接手别人用过的电脑&#xff0c;Windows 的临时文件、浏览器缓存、更新补丁残留、软件卸载后留在注册表里的失效键值&#xff0c;都会一点一点把系统盘和注册…

2026/10/21 4:48:54 阅读更多 →
ExtractBench 数据集解剖:一行 JSONL 里的 8 个字段与 JSON Schema 定义完全详解

ExtractBench 数据集解剖:一行 JSONL 里的 8 个字段与 JSON Schema 定义完全详解

【免费下载链接】ExtractBench 项目地址&#xff1a; https://ai.gitcode.com/hf_mirrors/llamaindex/ExtractBench 点击查看 免费下载 做文档信息抽取&#xff08;Information Extraction&#xff09;评测时&#xff0c;你是否想知道一个基准数据集到底长什么样&#xff1f;这…

2026/10/21 4:47:54 阅读更多 →
什么是regnety_032.tv2_in1k?新手完整指南:从零认识这款19.4M参数的轻量级图像分类模型

什么是regnety_032.tv2_in1k?新手完整指南:从零认识这款19.4M参数的轻量级图像分类模型

【免费下载链接】regnety_032.tv2_in1k 项目地址&#xff1a; https://ai.gitcode.com/hf_mirrors/timm/regnety_032.tv2_in1k 点击查看 免费下载 regnety_032.tv2_in1k 是一个基于 timm 框架的轻量级图像分类模型&#xff0c;属于 RegNetY 家族&#xff0c;仅有 19.4M&#x…

2026/10/21 4:47:54 阅读更多 →

最新新闻

大语言模型训练全流程解析:从Token到SFT与RLHF的工程实践

大语言模型训练全流程解析:从Token到SFT与RLHF的工程实践

1. 先建立整体坐标系&#xff1a;LLM的四大训练阶段与核心概念地图聊大语言模型之前&#xff0c;我觉得最重要的事不是急着看某个公式或者某个库的API&#xff0c;而是先把整个训练流程的“地图”装进脑子里。很多人一上来就盯着“上下文窗口”“Token”这些名词逐个查&#xf…

2026/10/21 6:59:47 阅读更多 →
知识工作插件化:从设计逻辑到工程落地的完整实践指南

知识工作插件化:从设计逻辑到工程落地的完整实践指南

1. 从"知识工作插件"这个命名说起&#xff1a;它到底在解决什么问题第一次看到 knowledge-work-plugins 这个命名&#xff0c;我的直觉是&#xff1a;这不是一个具体的工具&#xff0c;而是一类架构思路的统称。它指向的是把"知识工作"这件事拆解成可插拔、…

2026/10/21 6:59:47 阅读更多 →
深度学习训练诊断:从梯度消失到激活漂移的排查实践

深度学习训练诊断:从梯度消失到激活漂移的排查实践

一开始接触深度神经网络的时候&#xff0c;我最大的困惑不是“它能不能学”&#xff0c;而是“它到底是怎么学出来的”。前面几篇我们聊了网络结构、损失函数、正则化这些基础零件&#xff0c;但真到了调模型的时候你会发现&#xff0c;真正拉开差距的往往不是模型的骨架&#…

2026/10/21 6:59:47 阅读更多 →
储能与虚拟电厂收益关键:预测系统可信度如何构建

储能与虚拟电厂收益关键:预测系统可信度如何构建

最近这段时间&#xff0c;储能和虚拟电厂几乎成了能源行业最热的两条赛道&#xff0c;各种项目发布会、示范工程、聚合平台路演一个接一个。但圈内人私下聊起来&#xff0c;真正关心的不是谁的电池容量大、谁的界面做得炫&#xff0c;而是同一个问题&#xff1a;项目到底能不能…

2026/10/21 6:59:46 阅读更多 →
告别通用模板:答辩PPT的场景化应用策略拆解

告别通用模板:答辩PPT的场景化应用策略拆解

每到毕业季&#xff0c;答辩现场的“翻车”方式总是千奇百怪&#xff1a;有人PPT做得像产品发布会&#xff0c;评委想看的数据却一页都没有&#xff1b;有人内容明明很扎实&#xff0c;却因为版式太乱、逻辑断层&#xff0c;被误判为“准备不充分”&#xff1b;还有人开场就念稿…

2026/10/21 6:59:46 阅读更多 →
Linux网络学习【传输层协议UDP】

Linux网络学习【传输层协议UDP】

&#x1f525;承渊政道&#xff1a;个人主页 ❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C知识内容》 《Linux系统与网络知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》 《cpolar知识学习》 ✨…

2026/10/21 6:58:46 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →