ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Docker容器化部署:Spring Boot+MySQL+Redis实战记录

Docker容器化部署:Spring Boot+MySQL+Redis实战记录 跟着黑马程序员的Java Web课程走到项目部署这一章很多人都会卡在一个点本地IDEA里跑得好好好的项目一旦打包丢到服务器要么JDK版本对不上要么数据库连接失败再要么中文乱码、时区差八小时。这里面的核心矛盾就是开发环境和部署环境根本不在同一个频道。而Docker这套项目部署方案本质上就是把运行环境连同代码一起打包成一个镜像挪到任何机器上都能原样跑起来。这篇笔记记录了我把一个基于Spring Boot MyBatis的Java Web项目容器化的完整过程从MySQL、Redis到应用容器一步步打通最后用Docker Compose一键编排。适合刚学完Java Web、想亲手把项目部署上线的同学。1. 部署前的环境设计与架构梳理1.1 认清Java Web项目的部署形态传统Java Web项目部署一般要在一台服务器上手动装JDK、Tomcat、MySQL、Redis然后调环境变量、改配置文件再单独启动各个进程。这套流程最大的问题是“一次配置处处受罪”同一套代码在A机器是好的换到B机器就莫名奇妙经常是某个依赖库版本不一致或者系统字符集不同。Docker的解决思路是把“操作系统差异”这一层隔离掉——你的Java应用跑在一个固定基础镜像里它看到的环境永远一致。在做容器化规划之前先要明确项目是什么形态。如果你用的是传统SSH框架或者JSP项目最后产物是一个WAR包那就要考虑用Tomcat镜像把WAR包扔到webapps目录里。如果你用的是Spring Boot项目打包出来是一个可执行的JAR包里面已经内嵌了Tomcat就完全不需要再装一个独立Tomcat容器直接基于JDK镜像运行JAR包即可。我这次部署的就是Spring Boot项目所以第二阶段的镜像直接选openjdk:8-jre-alpine省掉一层Tomcat中间环节镜像体积更小排查问题也更少。这个选择背后是有理由的比起“装好Tomcat再放WAR”的老路子Spring Boot内置容器的部署方式天然更适合容器化镜像里只需要JRE不需要额外补充Tomcat依赖。如果你的课程课件里还在用Tomcat启动建议课下顺手把项目改造成Spring Boot的java -jar启动方式后面接Docker会顺滑很多。1.2 Docker环境初始化与国内镜像加速工欲善其事必先利其器。部署之前先把Docker引擎装好。Linux服务器上我通常直接用官方脚本安装curl -fsSL https://get.docker.com | bash systemctl enable docker systemctl start docker安装完成之后确认版本docker version这里有一个在实操中很容易被忽略的环节拉取镜像的速度。Docker Hub在国内的访问并不稳定如果直接docker pull mysql:8.0经常等半天不动。我的做法是配置国内镜像加速器比如去阿里云控制台申请一个专属加速地址然后写入/etc/docker/daemon.json{ registry-mirrors: [https://your-id.mirror.aliyuncs.com] }改完重启Dockersystemctl daemon-reload systemctl restart docker如果你是在Windows上学习用Docker Desktop一般会自动配置好基础环境但前提是BIOS里开启虚拟化并且启用WSL2。不少同学安装Docker Desktop之后发现启动失败提示“virtualization support not detected”十有八九就是WSL2没装或者虚拟化没开。此时先检查Windows功能里“适用于Linux的Windows子系统”和“虚拟机平台”是否勾选。1.3 规划目录与数据卷容器是一个“用完即走”的进程容器内写文件之后容器一删除数据就没了。所以部署Java Web项目在一开始就要规划清楚哪些目录数据可以丢哪些数据绝对不能丢。我的习惯是在宿主机建一个统一的目录比如/opt/study-app里面按组件分目录存放MySQL的数据文件、Redis的持久化文件、Nginx的日志等。mkdir -p /opt/study-app/mysql/data mkdir -p /opt/study-app/redis/data mkdir -p /opt/study-app/web后面启动容器时用-v参数把这些目录挂载进容器这样即使容器重建数据库数据还在。这是整个部署过程中最值得养成的好习惯很多同学第一次部署项目容器删了一瞬间半年开发数据全没了那种酸爽谁经历谁知道。2. Java Web应用镜像构建Dockerfile与镜像优化2.1 一份可复用的Java应用Dockerfile构建应用镜像是整个部署链条里的关键环节。我建议直接用多阶段构建先把Maven构建阶段和运行阶段分开。这么做的好处非常明显最终镜像里不会残留Maven仓库、编译中间文件体积能缩小一大截。下面这份Dockerfile在开源项目里非常常见也是我实际项目里一直在用的模板# 第一阶段构建 FROM maven:3.8.6-openjdk-8 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM openjdk:8-jre-alpine WORKDIR /app COPY --frombuilder /app/target/demo-0.0.1-SNAPSHOT.jar app.jar ENV TZAsia/Shanghai RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime echo Asia/Shanghai /etc/timezone EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]第一阶段里RUN mvn dependency:go-offline -B这一步容易被人忽略但它是速度优化利器。它的作用是提前把项目需要的所有依赖拉取并缓存避免每次构建镜像时都联网重新拉一遍依赖。之后COPY src ./src和RUN mvn clean package -DskipTests才会真正执行源码编译打包。第二阶段里ENV TZAsia/Shanghai和时区软链接必须一起做否则Java应用运行后获取系统时间会快8个小时日志时间和数据库时间经常对不上排查问题特别痛苦。如果你用的是openjdk:8-jre-alpine这种基础镜像它默认不带完整tzdata所以必须手动创建时区软链接。2.2 镜像构建的常见误区和体积优化构建Java镜像时很多人会直接拿完整JDK镜像跑jar包比如openjdk:8-jdk一个镜像就几百MB。实际上运行JAR包只需要JRE用openjdk:8-jre-alpine即可。如果你的项目用到了某些需要编译期功能的组件比如JSP动态编译那才需要保留JDK否则JRE完全够用。还有一个小习惯就是给项目根目录加一份.dockerignore文件把不相关的内容排除出构建上下文target/ .git/ .gitignore *.iml .idea/ logs/不加这个文件docker build时Docker会把整个目录都发送给服务端做上下文目录里有几百MB的target包构建过程会明显变慢。加了这个文件Docker引擎只会看到真正需要参与构建的源码和配置文件。构建命令和单容器启动命令也很简单docker build -t demo:1.0 . docker run -d --name demo-web -p 8080:8080 demo:1.0-d表示后台运行-p 8080:8080把宿主机的8080端口映射到容器的8080端口。启动完成后用curl http://localhost:8080验证或者直接浏览器打开服务器IP加端口号。这一步如果通过说明应用镜像本身没问题接下来要把MySQL和Redis也拉进同一套环境。2.3 从“能跑”到“跑得明白”常用镜像排查命令镜像构建和启动阶段遇到问题不要急着翻文档先看容器状态和日志。我的顺序固定是docker ps -a # 看所有容器状态 docker logs --tail 200 demo-web # 看最近200条日志 docker inspect demo-web # 看容器内配置细节大多数启动失败都能在日志里直接看到原因比如“Caused by: java.net.ConnectException”说明连不上数据库“No such file or directory”说明JAR包路径不对。容器日志是整个部署环节的“第一现场”比到处问人要靠谱得多。3. 数据库与缓存容器化MySQL与Redis的部署要点3.1 MySQL容器化与数据持久化Java Web项目基本离不开MySQL。这里最核心的一条原则数据库容器必须做数据持久化。我用的是MySQL 8.0启动命令如下mkdir -p /opt/study-app/mysql/data mkdir -p /opt/study-app/mysql/conf mkdir -p /opt/study-app/mysql/logs docker run -d --name study-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -v /opt/study-app/mysql/data:/var/lib/mysql \ -v /opt/study-app/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /opt/study-app/mysql/logs:/var/log/mysql \ --restartalways \ mysql:8.0环境变量MYSQL_ROOT_PASSWORD是MySQL官方镜像规定的root密码入口第一次初始化容器时必须指定。--restartalways非常推荐它让容器在Docker重启或宿主机重启后自动拉起不至于一次故障就永久掉线。my.cnf里我通常写死字符集这是中文乱码问题的根治手段[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci default-time-zone08:00 [client] default-character-setutf8mb4如果不设置character-set-serverMySQL 8.0容器默认的字符集是latin1Java应用插入中文就会出现问号。UTF8MB4比UTF8多了对emoji等四字节字符的支持也是目前互联网项目的标准配置。3.2 Redis容器化与密码配置Redis在Java Web项目里一般承担缓存、会话共享等职责。Redis容器启动相对简单但有一点要注意你没法用环境变量直接设置Redis密码需要在启动命令里显式加参数或者挂载配置文件。我用的是挂载配置文件的方式mkdir -p /opt/study-app/redis/conf vim /opt/study-app/redis/conf/redis.conf配置文件里至少包含以下内容appendonly yes requirepass 123456appendonly yes开启AOF持久化这样Redis宕机重启后缓存数据丢失不会太离谱。requirepass是访问密码生产环境不设密码相当于裸奔。然后启动容器docker run -d --name study-redis \ -p 6379:6379 \ -v /opt/study-app/redis/data:/data \ -v /opt/study-app/redis/conf/redis.conf:/etc/redis/redis.conf \ redis:7.0 redis-server /etc/redis/redis.conf注意命令末尾的redis-server /etc/redis/redis.conf它的含义是让容器启动时用我们自己挂载进去的配置文件而不是官方默认配置。不加这个参数Redis启动后就是裸配置密码和持久化全都不生效。3.3 容器间网络通信告别localhost这一节是新手翻车率最高的地方。很多人习惯在Java配置文件里写localhost:3306或127.0.0.1:3306然后放到容器里运行结果应用容器疯狂报“Connection refused”。原因很简单每个容器有独立的网络命名空间应用容器里的localhost指的是它自己根本不是MySQL容器。解决这个问题的标准姿势是使用Docker自定义网络。先创建一个bridge网络然后让所有相关容器都加入这个网络docker network create study-net docker run -d --name study-mysql --network study-net \ -e MYSQL_ROOT_PASSWORD123456 \ -v /opt/study-app/mysql/data:/var/lib/mysql \ mysql:8.0 docker run -d --name study-redis --network study-net \ -v /opt/study-app/redis/data:/data \ redis:7.0 redis-server /etc/redis/redis.conf docker run -d --name study-web --network study-net \ -p 8080:8080 \ demo:1.0在study-net这个网络里容器之间可以通过容器名互相访问。于是Java配置文件里数据库地址写成jdbc:mysql://study-mysql:3306/demoRedis地址写成study-redis:6379。端口映射依然保留3306:3306和6379:6379方便开发时用本机工具连上去查看。排序顺序并不重要容器只要在同一个网络就能用服务名互相解析。这个“用服务名代替IP”的模型是后面Docker Compose能一键编排的基础。4. 一键编排上线Docker Compose实战4.1 用docker-compose定义整套环境手动敲三条docker run命令还能接受但每次更新项目都要重新敲一遍既容易漏参数也不利于团队协作。Docker Compose的价值是把整个部署环境定义成一份声明式文件提交到Git仓库别人克隆下来后一条命令就能启动整套服务。我把上面的部署方案整理成一份docker-compose.ymlversion: 3.8 services: mysql: image: mysql:8.0 container_name: study-mysql restart: always environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: demo TZ: Asia/Shanghai ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf networks: - study-net redis: image: redis:7.0 container_name: study-redis restart: always command: redis-server --appendonly yes --requirepass 123456 ports: - 6379:6379 volumes: - redis-data:/data networks: - study-net web: build: context: . dockerfile: Dockerfile container_name: study-web restart: always ports: - 8080:8080 depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod networks: - study-net volumes: mysql-data: redis-data: networks: study-net:这份文件里有几个细节值得注意。MYSQL_DATABASE: demo让MySQL容器在首次初始化时自动创建一个名为demo的空数据库省去了手动执行的SQL脚本。depends_on只是控制启动顺序不会让web容器等到MySQL完全就绪才启动。Spring Boot项目通常会自动重试连接数据库所以问题不大。volumes段落声明了具名卷数据会被Docker托管在宿主机容器删除后卷还在。4.2 启动、更新与数据导入首次启动整套环境的命令docker compose up -d --build--build会强制重新构建镜像确保web镜像就是当前代码最新的状态。启动后查看所有服务的状态和日志docker compose ps docker compose logs -f项目迭代后更新代码的流程也很固定先改代码然后重新构建镜像再仅重启web服务docker compose up -d --build web这个命令只会重建和启动web服务MySQL和Redis不受影响数据当然也安全。如果部署的是新项目且需要初始化SQL脚本把.sql文件复制进MySQL容器然后执行即可docker cp init.sql study-mysql:/tmp/init.sql docker exec -it study-mysql mysql -uroot -p123456 demo /tmp/init.sql这一步我也经常用尤其是用docker compose部署旧项目时数据库迁移脚本一跑整个项目就起来了。4.3 日常维护与进程管理日常维护中最常用的命令是查看日志、重启服务、进入容器排查docker compose logs --tail 100 web docker compose restart web docker exec -it study-web sh进入容器之后可以在容器内部执行ps、cat、ls等命令确认Java进程是否还在、配置文件是否如预期。需要提醒的是很多轻量基础镜像没有bash只有sh所以进入容器时优先用sh而不是bash。docker compose down会停止并删除容器但默认不会删除卷。如果你手滑加了-v参数比如docker compose down -v那MySQL和Redis的数据卷也会被一起删掉这是真正的“删库跑路”操作。我在生产环境基本上只用docker compose down从不敢带-v。5. 常见问题与排查技巧实录5.1 容器启动失败怎么办部署过程中遇到容器启动失败先别慌按顺序看三样东西状态、日志、端口。比如docker ps -a显示Exited (1)直接看日志docker logs study-web日志里最常见的是这么几种情况Port is already allocated端口冲突。先用ss -lnp | grep 8080找出占用端口的进程如果宿主机上已经有一个应用占着8080就把容器映射端口改成其他端口。Error: Unable to access jarfile app.jarJAR包路径不对或者Dockerfile里COPY阶段的目标文件名没对上。检查target目录下真实生成的JAR包名别想当然用通配符。Caused by: org.springframework.jdbc.Cant connect to MySQL server应用容器访问不到MySQL。重点检查是不是同一个网络以及连接串写的是容器名还是localhost。排查熟练之后基本一眼就能定位问题方向。建议做一张“问题-原因-解法”的小卡片贴在工位上省得每次翻聊天记录。5.2 数据库连接失败与中文乱码排查数据库连接失败除了网络问题还有两类经典原因。一类是密码错误导致Access denied for user这种情况检查MYSQL_ROOT_PASSWORD是否和Java配置一致同时看MySQL容器内用户权限。容器初始化时如果没指定数据库后面手工建库也要记得授权。另一类是连接超时表现为Communications link failure。除了网络因素还要确认MySQL容器有没有绑定到宿主机端口防火墙是否放行3306。如果你是跨服务器访问数据库比如开发机直连服务器上的MySQL容器就要保证服务器安全组和防火墙开放3306端口。中文乱码的排查顺序是先看MySQL容器内部默认字符集再确认数据库和表字符集最后看应用连接串有没有加characterEncodingutf8。MySQL容器内部字符集可以通过SHOW VARIABLES LIKE character_set%;确认。如果看到全是latin1赶紧对照我上面写的my.cnf挂载配置重建MySQL容器。5.3 时区、磁盘与资源限制问题时区问题前面提过这里再补一个细节Spring Boot应用里如果数据库连接串没有带serverTimezoneAsia/Shanghai而MySQL容器又设置了TZAsia/ShanghaiJava读取时间时还是可能差8个小时。所以JDBC连接串我一般写成这样jdbc:mysql://study-mysql:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai三项到位基本不会出现时间偏差。磁盘问题是运维阶段的隐藏炸弹。构建镜像、容器日志、MySQL数据都会持续占用磁盘。我每周会固定做一次镜像清理docker system df docker image prune -f docker system prune -f第一条命令能直观看到镜像、容器、卷、缓存各占多少磁盘。第二条是清理悬空镜像第三条会删除所有停止的容器和未使用的网络对于长期开发机非常有效。资源限制也很重要。如果你在一台只有2G内存的云服务器上同时跑MySQL、Redis、Java应用很容易把服务器跑卡死专业点叫OOM。Docker Compose里可以给每个服务限制内存和CPUservices: web: deploy: resources: limits: cpus: 1.0 memory: 512M这个配置放在compose文件里容器创建之后会强制限制资源占用避免一个失控进程拖垮整台服务器。5.4 问题排查速查表最后把部署Java Web项目到Docker时最常踩的坑整理成一张速查表方便你以后直接对照查。故障现象常见原因排查/解决方法容器一直重启应用启动失败或健康检查不通过docker logs看日志确认依赖服务是否先启动好应用连不上MySQL没有使用同一个Docker网络加入同一自定义网络连接串用容器名MySQL Access denied密码不一致或用户权限问题检查MYSQL_ROOT_PASSWORD确认账号权限中文插入变问号服务端字符集不是utf8mb4挂载my.cnf重建MySQL容器时间差8小时基础镜像默认UTC时区Dockerfile设置ENV TZAsia/ShanghaiJDBC加serverTimezone镜像体积巨大用了完整JDK而不是JRE多阶段构建运行阶段用openjdk:8-jre-alpineDocker启动报虚拟化错误Windows未开启虚拟化/WSL2检查BIOS虚拟化启用Windows虚拟机和WSL2port is already allocated宿主机端口被占用换端口或杀掉占用进程最后聊一点我自己的体会。这套Docker部署流程跑顺之后再回头看项目部署这件事核心其实只有四个字镜像、容器、网络、卷。把镜像想成“可移动环境”容器是“运行中的实例”网络解决“容器之间互相找得到”卷解决“容器删了数据还在”。这四个概念一次性想通之后不管是换机器、换云服务器还是项目交接都只是复制一份文件再来一遍的事。我在部署过程中踩得最深的坑就是数据中心化思维没转过来以前喜欢直接进容器里改配置文件后来才发现镜像重打、数据挂载才是最干净的路。如果你也在学黑马程序员这套课程的部署章节建议不要只是看亲手把Dockerfile和Compose文件写一遍。下次再遇到“本地能跑、服务器挂了”这类问题你应该能笑着把它送走。
返回列表