尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Docker部署Spring Boot项目全攻略:从Dockerfile到Compose实战

Docker部署Spring Boot项目全攻略:从Dockerfile到Compose实战 Docker部署Spring Boot项目这件事我前前后后折腾了不少次。从最早在测试环境手动装JDK、配置MySQL到后来用Docker Compose一键拉起整套环境中间踩过的坑确实不少。这篇东西不整虚的直接把一套完整可用的方案写出来按着做就能把Spring Boot应用跑在容器里。先说清楚这篇博文适合谁已经会写Spring Boot接口但没怎么碰过Docker的Java开发或者是团队里需要自己负责部署环节、又不想啃长篇官方文档的同学。我会把Docker最核心的几个概念串起来讲不会扯太多用不上的理论重点全放在“怎么把项目跑起来”这件事上。1. 部署思路与整体方案设计1.1 为什么要把Spring Boot应用容器化很多刚接触部署的同学第一反应是我直接打一个Jar包扔到服务器上装个JDK用java -jar跑起来不就行了确实可以但一旦环境复杂起来问题就来了。举个例子你本地开发用的JDK 17测试服务器装的是JDK 8跑起来直接报UnsupportedClassVersionError。或者同一个服务器上跑了两个项目一个依赖Python 2、一个依赖Python 3互相干扰。更别提MySQL、Redis这些中间件每台新机器都要手动装一遍版本对不上又是一堆破事。Docker解决的核心问题就一个字隔离。它把应用连同运行环境一起打包成镜像不管底层机器是什么系统、装了什么软件只要装了Docker就能把应用跑起来。我个人的直观感受是在Docker时代部署这件事从“环境工程师的活”变成了“复制粘贴几条命令的事”。这也是为什么现在大多数公司在招聘要求里都写了“熟悉Docker”的原因。1.2 镜像与容器的类比理解Docker里最基础的两个概念是镜像和容器。很多人搞不清它们的关系我用个生活化的类比来说。镜像就好比一个做蛋糕的模具容器是用模具做出来的蛋糕。模具本身是固定的一份模具可以做出无数个完全一样的蛋糕每个蛋糕是独立存在的一个坏了不影响另一个。你对某个容器做的任何修改比如往里面装了个软件都不会影响原来的镜像除非专门用docker commit去生成新镜像。在实际操作中我们的流程就是先写一个Dockerfile描述怎么构建镜像然后构建出镜像再用镜像去启动一个或多个容器。镜像用于交付容器用于运行这个思维一定要建立起来。1.3 部署方案选型Jar包直跑对比容器化我用一张表把两种方式的区别列出来大家在选方案的时候心里有数对比维度直接部署Jar包Docker容器化部署环境一致性依赖每台机器手动配置镜像自带运行环境完全一致多应用隔离弱依赖版本容易冲突强各容器互不影响部署速度需要装JDK、配置环境变量拉镜像、跑命令分钟级完成版本回滚需要重新上传Jar包用旧镜像重新启动即可资源占用相对较小略有额外开销但可接受运维成本机器多了之后很难管理统一通过Docker命令管理从我的经验来看单机部署一个小项目两种方式差别不大但只要是两个以上的服务、或者需要用到数据库和缓存直接上Docker会省心很多。这篇博文讲的就是完整的一套容器化方案包括应用本身的镜像构建以及配套的MySQL、Redis容器部署。2. 环境准备Docker安装与启动2.1 Windows环境安装Docker DesktopWindows上装Docker官方推荐的是Docker Desktop。有个前提条件你的CPU必须支持虚拟化并且在BIOS里把虚拟化打开。热搜词里有一条“docker desktop failed to start because virtualisation support wasnt detected”说的就是这个问题。遇到这个报错先别急着重装软件按下面顺序排查按CtrlShiftEsc打开任务管理器切到“性能”标签看右下角“虚拟化”是不是“已启用”。如果是“已禁用”重启电脑进BIOS开机时按Del或F2不同品牌按键不一样找到Intel VT-x或AMD SVM相关的选项设为Enabled。确保Windows的Hyper-V功能是开着的。在PowerShell管理员模式里执行Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All安装WSL2。Docker Desktop默认依赖WSL2来跑Linux内核这一步很多人漏掉。管理员身份打开PowerShell执行wsl --install装完重启然后运行wsl --set-default-version 2确认用的是WSL2。这两步都弄好之后再启动Docker Desktop基本就正常了。启动成功后右下角的小鲸鱼图标会变成稳定的状态不再是转圈或者报错。2.2 Linux环境安装Docker服务器上部署的话一般用Linux。以CentOS 7和Ubuntu 20.04为例安装命令并不复杂。CentOS 7sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install -y docker-ce docker-ce-cli containerd.io sudo systemctl start docker sudo systemctl enable dockerUbuntusudo apt update sudo apt install -y apt-transport-https ca-certificates curl software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository deb [archamd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable sudo apt update sudo apt install -y docker-ce sudo systemctl start docker sudo systemctl enable docker这里多说一句在国内服务器上拉镜像可能会比较慢建议配置镜像加速器。在/etc/docker/daemon.json里加上加速地址比如各云厂商提供的加速服务然后重启Dockersudo systemctl daemon-reload sudo systemctl restart docker2.3 验证Docker是否安装成功装完之后执行下面这条命令如果能正常输出版本信息说明Docker核心组件没问题docker version再执行一条更直观的测试拉取并运行一个hello-world镜像docker run hello-world看到“Hello from Docker!”的输出说明整个链路拉镜像、创建容器、运行容器都是通的。这一步做成这样环境准备就算完成了。3. 准备Spring Boot项目从源码到可部署的Jar包3.1 项目结构与Maven配置的关键点Docker部署的输入物是Jar包所以第一步是把Spring Boot项目打包成可执行的Jar。这里有一个核心关键点pom.xml里必须配置Spring Boot的Maven插件否则打出来的Jar包不能通过java -jar直接运行。一个标准的Spring Boot项目pom.xml里至少要包含这些内容parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build关于Spring Boot版本多说一句如果你用的Spring Boot 3.x那对应JDK版本必须是17及以上Spring Boot 2.x系列JDK 8和JDK 11都能跑。很多同学一上来就建了一个Spring Boot 3.2的项目结果本机JDK是8编译都不通过。创建项目之前先确认好JDK版本这一点不夸张地说能帮你省下半天时间。我自己的习惯是Java 8配Spring Boot 2.7.xJava 17配Spring Boot 3.x稳得很。3.2 用Maven命令打出可执行Jar包打包很简单在项目根目录执行mvn clean package -DskipTests这里-DskipTests的意思是跳过单元测试加快打包速度。如果你用的是IDEA右侧Maven工具窗口里双击package也是一样的效果。打包完成后在target目录下会看到一个xxx.jar文件比如demo-0.0.1.jar。这里注意区分Spring Boot的Maven插件会生成两个Jar一个是xxx.jar可执行一个是xxx.jar.original普通Jar仅包含项目代码。部署时用前者。3.3 部署前的本地自测在打镜像之前强烈建议先在本机用Java直接跑一次Jar包确认代码本身没问题。本地测试命令java -jar target/demo-0.0.1.jar看到类似下面的日志说明Spring Boot应用启动成功Tomcat started on port(s): 8080 (http) with context path Started DemoApplication in 2.5 seconds这一步非常有用。如果直接在Docker里跑才发现代码报错排查起来会多一层容器日志的干扰效率低很多。本地先验证再进Docker绝对是省时间的习惯。4. 编写Dockerfile镜像构建的核心文件4.1 一份可直接用的Dockerfile逐行解析Dockerfile是构建镜像的“说明书”每一行都是一个指令。下面这份是我在项目里常用的注释写得很清楚# 基础镜像使用带JDK 8的镜像 FROM openjdk:8-jdk-alpine # 维护者信息 MAINTAINER yournameexample.com # 设置工作目录 WORKDIR /app # 把本地的Jar包复制到镜像里的/app目录并改名为app.jar COPY target/demo-0.0.1.jar app.jar # 暴露应用的端口 EXPOSE 8080 # 启动命令运行Jar包 ENTRYPOINT [java, -jar, app.jar]这份Dockerfile的逻辑很好理解找一个带JDK 8的镜像作为基础把项目Jar包放进去最后用java -jar启动。如果你的项目用的是JDK 17把第一行换成FROM openjdk:17-jdk-alpine即可。这里有个细节要用好基础镜像里的JDK版本必须和项目编译时用的JDK版本一致或更高。比如你本地用JDK 17编译的类放到JDK 8的镜像里跑启动时直接给你抛UnsupportedClassVersionError这个错误提示其实很清楚但在线上排查一圈才定位到是版本问题的情况还挺常见的。4.2 基础镜像选择版本匹配与体积控制openjdk:8-jdk-alpine和openjdk:17-jdk-alpine是两种最常用的基础镜像优点是体积小Alpine Linux本身很精简镜像只有几十MB。但有一个坑Alpine镜像用的是musl libc极少数依赖native library的Java库在Alpine上会有兼容问题。如果你用到了一些带JNI调用的库比如某些加密组件、OCR引擎建议改用eclipse-temurin:8-jdk或eclipse-temurin:17-jdk这类基于Ubuntu的标准镜像。体积会大一些但兼容性更好。宁可用体积大一点的镜像换稳定性也不要在部署环节省那几十MB。4.3 时区、JVM参数与健康检查的补充配置基础版Dockerfile能跑但放到生产环境还有几个点要处理。时区问题默认容器时区是UTC和国内差8个小时。日志里时间对不上会非常难受。解决办法是在Dockerfile里加一行ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezoneAlpine镜像里如果提示找不到/usr/share/zoneinfo先执行apk add tzdata装一下时区数据。JVM参数可以在启动命令里加上内存限制防止容器把宿主机内存吃满ENTRYPOINT [java, -Xms256m, -Xmx512m, -jar, app.jar]-Xms是初始堆内存-Xmx是最大堆内存。具体值根据你的服务负载来定我一般给到内存上限的一半左右留些余量。健康检查如果你的服务会被服务发现组件比如Consul、Nacos管理健康检查很有必要。可以在Dockerfile里加HEALTHCHECK --interval30s --timeout3s --retries3 CMD wget -q -O - http://localhost:8080/actuator/health || exit 1底层原理是每隔30秒请求一次Spring Boot的Actuator健康检查接口连续3次失败就标记容器不健康方便上层调度器处理。这个功能依赖Spring Boot的spring-boot-starter-actuator依赖记得在pom里加上。4.4 多阶段构建让镜像里不残留编译工具上面写的Dockerfile有一个小问题基础镜像里同时包含了JDK和编译相关的工具体积偏大。对于追求镜像体积的场景可以用多阶段构建。# 第一阶段用Maven镜像编译项目 FROM maven:3.8-openjdk-8 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段只保留运行环境和Jar包 FROM openjdk:8-jre-alpine WORKDIR /app COPY --frombuilder /build/target/demo-0.0.1.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]第一阶段用带Maven的镜像把项目编译成Jar包第二阶段只拿Jar包和JRE运行环境。这样最终镜像不包含Maven和JDK编译工具体积能小一半以上。如果你的服务器磁盘紧张或者镜像需要频繁上传到镜像仓库多阶段构建非常值得用。5. 构建镜像与运行容器核心操作实战5.1 使用docker build命令构建镜像Dockerfile写好后在项目根目录Dockerfile所在目录执行docker build -t demo-app:1.0.0 .参数说明-t给镜像起一个名字和标签格式是名称:标签。我用1.0.0作为版本号方便后面做版本管理。.构建上下文路径表示把当前目录下的所有文件都发给Docker守护进程作为构建的输入。构建过程中每个步骤都会输出日志比如[] Building 5.2s表示这一层构建耗时。构建完成后执行docker images就能看到刚构建的镜像docker images REPOSITORY TAG IMAGE ID CREATED SIZE demo-app 1.0.0 abc123def456 10 seconds ago 124MB这里说明一点构建上下文尽量不要包含target目录这类临时产物可以在项目根目录创建.dockerignore文件写法和.gitignore类似target/ *.iml .idea/这样构建时不会把这些大文件传给Docker守护进程构建速度会快不少。5.2 使用docker run命令启动容器镜像构建好之后启动容器就一条命令docker run -d \ --name demo-app \ -p 8080:8080 \ --restartalways \ demo-app:1.0.0逐项拆解一下-d后台运行容器。不加这个参数容器会占据当前终端CtrlC容器就停了。--name demo-app给容器起个名字之后管理都用这个名字。-p 8080:8080端口映射宿主机8080端口转发到容器内的8080端口。格式是宿主机端口:容器端口。--restartalways容器异常退出时自动重启服务器重启后也会跟着拉起生产环境很实用。启动之后用docker ps查看运行状态docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8f2a1b3c4d5e demo-app:1.0.0 java -jar app.jar 5 seconds ago Up 5 seconds 0.0.0.0:8080-8080/tcp demo-app看到STATUS是Up状态说明容器正常运行。这时候在浏览器访问http://localhost:8080就能看到你的Spring Boot项目接口了。5.3 容器常用操作命令速查部署过程中最常用的命令就这几个命令作用docker logs -f demo-app查看容器日志-f表示持续输出docker exec -it demo-app /bin/sh进入容器内部相当于SSH到容器里docker restart demo-app重启容器docker stop demo-app停止容器docker rm demo-app删除容器需先停止docker rmi demo-app:1.0.0删除镜像日志查看是排查问题的第一入口。启动时如果容器秒退先看日志docker logs demo-app如果日志里出现类似APPLICATION FAILED TO START说明代码或配置有问题按提示排查基本能定位。6. 依赖中间件的容器化部署MySQL与Redis6.1 部署MySQL 8.0容器并配置数据持久化一个完整的Spring Boot项目基本都离不开数据库。用Docker部署MySQL比手动安装简单太多但有一个点必须处理好数据持久化。容器一旦被删除里面存储的数据都会丢失。解决办法是挂载数据卷把容器里的数据目录映射到宿主机上。启动MySQL 8.0容器的命令docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123456 \ -e MYSQL_DATABASEtestdb \ -v /data/mysql/data:/var/lib/mysql \ -v /data/mysql/conf:/etc/mysql/conf.d \ --restartalways \ mysql:8.0参数说明-e MYSQL_ROOT_PASSWORDroot123456设置root用户的密码。-e MYSQL_DATABASEtestdb自动创建一个名为testdb的数据库。-v /data/mysql/data:/var/lib/mysql将宿主机/data/mysql/data目录挂载到容器的数据目录这样即使容器删了数据还在宿主机上。启动后在Spring Boot的application.yml里配置数据源spring: datasource: url: jdbc:mysql://localhost:3306/testdb?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root123456 driver-class-name: com.mysql.cj.jdbc.Driver这里url里的serverTimezone一定要设置否则用高版本的JDBC驱动会报时区错误。6.2 部署Redis容器并配置缓存依赖Redis在现代Web应用里基本是标配。启动Redis容器也是同样的套路docker run -d \ --name redis7 \ -p 6379:6379 \ -v /data/redis/data:/data \ --restartalways \ redis:7.0 redis-server --appendonly yes--appendonly yes开启AOF持久化配合数据卷挂载Redis数据基本不会丢。Spring Boot里配置Redis连接spring: redis: host: localhost port: 6379 database: 0 timeout: 3000ms6.3 应用容器与数据库容器之间的网络通信这里有一个细节值得注意如果Spring Boot应用和MySQL都跑在容器里应用容器内的localhost指向的是容器自己并不是宿主机。也就是说应用容器里不能通过jdbc:mysql://localhost:3306去连接宿主机上的MySQL容器会报连接拒绝。解决办法有三个第一种使用宿主机在Docker网络中的网关地址。在Linux上一般是172.17.0.1Windows/macOS上是host.docker.internal。第二种创建Docker网络让多个容器在同一个网络里用容器名互相访问。这个方案更优雅推荐使用docker network create app-network然后在启动容器时都加上--network app-network参数。这样应用容器里连接MySQL的地址就变成了url: jdbc:mysql://mysql8:3306/testdb这里的mysql8是MySQL容器的--name。用容器名通信Docker内置DNS会自动解析到容器IP运维上省心很多。第三种直接把application.yml里的数据库地址写成宿主机IP比如jdbc:mysql://192.168.1.100:3306/testdb。这个方法最简单但IP变了要改配置再打镜像不够灵活。在实际操作中我一般用第二种方式。多个服务组成一个内部网络互相用服务名访问跟微服务里的“服务发现”思路很像。7. 一键编排使用Docker Compose管理多服务7.1 为什么需要Docker Compose到了这一步你的部署环境里可能有三个容器应用容器、MySQL容器、Redis容器。如果每次启动都要一条条敲docker run命令不仅麻烦还容易出错。更尴尬的是容器之间的启动顺序没法保证MySQL还没起来应用就去连结果连不上。Docker Compose就是解决这个问题的用一个docker-compose.yml文件描述所有服务一条命令全部启动。7.2 编写docker-compose.yml的完整示例在项目根目录下创建docker-compose.yml内容如下version: 3.8 services: mysql8: image: mysql:8.0 container_name: mysql8 environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: testdb ports: - 3306:3306 volumes: - /data/mysql/data:/var/lib/mysql restart: always redis7: image: redis:7.0 container_name: redis7 command: redis-server --appendonly yes ports: - 6379:6379 volumes: - /data/redis/data:/data restart: always app: build: . container_name: demo-app depends_on: - mysql8 - redis7 ports: - 8080:8080 restart: always几个关键点解读一下services下定义了三个服务mysql8、redis7、app。每个服务对应一个容器。build: .表示app服务是从当前目录下的Dockerfile构建出来的镜像。depends_on声明app依赖mysql8和redis7Compose会先启动依赖的服务再启动app。但要注意depends_on只保证启动顺序不能保证数据库已经完全就绪。如果应用启动时连数据库偶发失败可以在Spring Boot里配置spring.datasource.hikari.initialization-fail-timeout或者用depends_on配合健康检查来彻底解决。简单起见把MySQL容器先启动等十几秒再启动应用也是一种土办法。7.3 Compose的常用操作命令写好docker-compose.yml后在文件所在目录执行docker compose up -d-d表示后台运行。这个命令会自动构建镜像如果服务里有build配置然后按依赖顺序启动所有容器。其他常用命令docker compose ps # 查看所有服务状态 docker compose logs -f app # 查看app服务的日志 docker compose down # 停止所有服务并删除容器 docker compose down -v # 停止服务并删除容器和卷数据卷里的数据会删除慎用 docker compose restart app # 重启单个服务使用Compose之后整个部署过程简化成了两步先写好docker-compose.yml再执行docker compose up -d所有服务全部起来。这套方案我用了很久稳定性很好日常维护成本基本为零。8. 常见问题与排查技巧实录8.1 典型报错速查表下面这些是我和团队同事在实际部署中遇到过的典型问题整理成表格方便大家对照排查报错信息原因分析解决方案UnsupportedClassVersionError编译Jar的JDK版本高于镜像里的JDK版本换一个版本匹配的基础镜像或者用低版本JDK重新编译Ports are not available: listen tcp 0.0.0.0:8080: bind: address already in use宿主机端口被占用netstat -tlnpdocker: Cannot connect to the Docker daemonDocker服务没启动Linux执行sudo systemctl start dockerWindows检查Docker Desktop状态Unable to find image openjdk:8-jdk-alpine locally本地没有该镜像拉取失败多为网络问题配置镜像加速器后重新docker pullFailed to determine a suitable driver class数据源配置没生效检查application.yml里的数据库连接配置确认配置类被扫描到Communications link failure应用连不上数据库确认连接地址如果通过容器名访问确认在同一网络确认数据库端口映射正确Caused by: java.net.UnknownHostException: mysql8应用和MySQL不在同一Docker网络重新创建网络确保两个容器都加--network参数或都在同一Compose项目里8.2 容器启动后立即退出的排查思路容器一启动就退出是最常见的故障。我排查这类问题的固定套路是第一步查看退出容器的日志docker logs demo-app第二步如果日志没内容用docker ps -a查看容器退出状态码。状态码是0说明是正常退出可能直接跑完了主进程就退出了非0说明有异常。第三步检查docker ps -a里显示的Exited (1)之类的状态码根据日志定位问题。一个很常见的坑Spring Boot应用启动失败时会打印APPLICATION FAILED TO START错误日志然后进程退出容器跟着退出。这是最典型的场景。解决方式是在本地直接跑Jar包复现问题定位更快。8.3 容器内日志时间与宿主机不一致的问题这个问题很多人会中招尤其是排查线上告警的时候。容器内默认时区是UTC宿主机可能是东八区日志时间对不上容易误导排查。解决方式前面已经提到在Dockerfile里加ENV TZAsia/Shanghai或者用docker run -e TZAsia/Shanghai传入时区环境变量。用Compose的话在服务配置里加environment: - TZAsia/Shanghai8.4 一个完整的排查案例实录有一次我在测试环境部署新的版本容器启动后访问接口一直超时。docker ps显示容器是Up状态但docker logs里只看到启动日志没有任何访问日志说明请求根本没到应用。挨个排查先看端口映射-p 8080:8080没问题再看防火墙宿主机8080端口没有放行。用curl http://localhost:8080测试直接提示连接失败。这时候检查Docker端口映射是否生效docker port demo-app输出正常的话再看宿主机端口监听netstat -tlnp | grep 8080发现8080端口根本没有进程监听。再仔细看docker ps发现容器端口那一列只显示8080/tcp没有0.0.0.0:8080-8080/tcp这种映射信息。原来是我启动容器时-p参数写错地方了被当成了环境变量。删掉容器按正确格式重新启动就好了。这类问题看上去低级但真的会发生。我的经验是遇到问题先看docker ps里的PORT列如果端口映射没生效后面所有排查都是白做。9. 一点实操沉淀这套方案我现在基本每天都在用对个人开发者或者小团队来说把Spring Boot应用、MySQL和Redis用Docker Compose管理起来已经够用且稳定。如果项目规模变大还能在这个基础上引入镜像仓库把构建好的镜像推到仓库里服务器上直接拉取、CI/CD提交代码后自动构建镜像并发布等工具但底层的东西还是这一套。最后有个建议刚开始练习部署时刻意把容器删了再启动几次或者故意用错误版本的基础镜像试一次多踩几次坑之后对Docker的理解绝对比看十篇教程都深。部署这件事最怕的是“只照着做不弄清原理”。把镜像、容器、数据卷这些基础概念揉碎了吃透后面遇到任何部署问题都能从容应对。
返回列表