从"本地能跑"到"生产级部署":Java + Docker 自动化部署深度复盘
- [前言:为什么要折腾 Docker?](#前言:为什么要折腾 Docker?)
- [0、 核心架构:构建标准化的交付链路](#0、 核心架构:构建标准化的交付链路)
-
-
- [1. 多阶段构建:极致优化镜像体积](#1. 多阶段构建:极致优化镜像体积)
- [2. Docker-Compose:定义服务间的拓扑关系](#2. Docker-Compose:定义服务间的拓扑关系)
-
- 一、这次实践我做了什么
- [二、 深度规范:项目交付的"仪式感"](#二、 深度规范:项目交付的“仪式感”)
-
- [1. 路由管理规范](#1. 路由管理规范)
- [2. 配置管理规范](#2. 配置管理规范)
- [3. 数据库初始化规范](#3. 数据库初始化规范)
- [4. 页面入口与部署验证规范](#4. 页面入口与部署验证规范)
- [5. 部署记录与环境追踪](#5. 部署记录与环境追踪)
- 三、实战突发:深刻的生产级问题排查
-
- [1. 本地能登录,部署后不能登录/注册](#1. 本地能登录,部署后不能登录/注册)
- [2. 改了 db.sql,服务器数据库却没更新](#2. 改了 db.sql,服务器数据库却没更新)
- [3. 本地环境和部署环境差异被放大](#3. 本地环境和部署环境差异被放大)
- [4. 线上运行一段时间后,登录/注册突然卡顿报错(深度复盘)](#4. 线上运行一段时间后,登录/注册突然卡顿报错(深度复盘))
- [四、 总结与展望](#四、 总结与展望)
自动化 Docker 部署实践总结
前言:为什么要折腾 Docker?
这次实践是我第一次完整地将 Java (Spring Boot) + MySQL 项目实现容器化自动化部署。
以前觉得"代码写完、本地运行没 Bug"或者Linux环境下下手动部署就算结束了。但这次我意识到,真正要让项目上线,环境配置、容器编排、数据库持久化、系统运维排查才是决定项目能否稳定运行的关键。
0、 核心架构:构建标准化的交付链路
1. 多阶段构建:极致优化镜像体积
我采用 Multi-stage Build 策略编写 Dockerfile。这种方式将"编译环境"与"运行环境"彻底分离。
dockerfile
# Stage 1: Build (编译阶段)
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 2: Run (运行阶段)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
技术思考:
- 隔离性:避免了在生产镜像中包含源代码和 Maven 仓库,提高了安全性。
- 轻量化 :使用
alpine基础镜像,将原本几百 MB 的镜像压缩到了最小,显著提升了分发速度。
2. Docker-Compose:定义服务间的拓扑关系
通过容器编排,我实现了应用与数据库的"一键拉起"。
yaml
services:
db:
image: mysql:8.0
container_name: java_gobang_db
environment:
MYSQL_ROOT_PASSWORD: "Your_Password"
MYSQL_DATABASE: java_gobang
volumes:
- ./src/main/db.sql:/docker-entrypoint-initdb.d/init.sql
- mysql_data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
app:
build: .
container_name: java_gobang_app
ports:
- "8081:8081"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/java_gobang?allowPublicKeyRetrieval=true
depends_on:
- db
一、这次实践我做了什么
- 用 Dockerfile 完成应用容器化
我先把原本只能在本地 IDE 里运行的项目,改造成可以通过镜像直接启动的应用。
dockerfile
# Stage 1: Build
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 2: Run
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
我这里采用了多阶段构建:
- 前一阶段负责用 Maven 编译打包
- 后一阶段只保留运行环境和 jar
这样镜像更干净,也更适合部署。
这一步的意义是:我把"本地 Java 项目"变成了"服务器可直接运行的 Docker 镜像"。
- 用 docker-compose.yml 编排应用和数据库
为了避免手动装 MySQL、手动导库、手动启动后端,我又把应用和数据库统一交给 docker-compose 管理。
yaml
services:
db:
image: mysql:8.0
container_name: java_gobang_db
environment:
MYSQL_ROOT_PASSWORD: "120xxxxxxxRn."
MYSQL_DATABASE: java_gobang
ports:
- "3306:3306"
volumes:
- ./src/main/db.sql:/docker-entrypoint-initdb.d/init.sql
- mysql_data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
app:
build: .
container_name: java_gobang_app
ports:
- "8081:8081"
environment:
- SERVER_PORT=8081
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/java_gobang?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=1203728050Rn.
depends_on:
- db
restart: always
volumes:
mysql_data:
这一步完成了几件事:
- 自动启动 MySQL 容器
- 自动启动 Spring Boot 容器
- 通过服务名
db建立容器间通信 - 首次启动时自动执行
db.sql - 通过数据卷保存数据库数据
- 通过 8081 对外提供访问
从我的理解看,这一步才真正体现了"自动化部署": 以前我要手动做很多事,现在只需要一条 docker-compose up -d --build。
二、 深度规范:项目交付的"仪式感"
这次我在"项目规范"上补上的内容:
这次不只是多了两个 Docker 文件,其实也顺带把项目结构和部署规范理清了。
1. 路由管理规范
统一入口 :新增 IndexController 实现根路径 / 自动跳转,确保用户访问体验。
我新增了根路径入口控制,让用户访问 / 时统一跳转到登录页,而不是暴露一个不清晰的默认入口。
java
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "redirect:/login.html";
}
}
从这次实践里,我自己总结出的路由规范是:
- 页面入口路由 和 接口路由 要分开
- 根路径
/需要有统一入口,不要让用户自己猜访问哪个页面 - 静态页面可以走
/login.html - 业务接口继续走
/login、/register这类后端 API
这样做的好处是:用户访问入口清晰,部署后测试也更方便,前端页面路由和后端业务接口职责更明确。
2. 配置管理规范
配置动静分离 :本地开发使用 application-local.yml,生产环境则通过 docker-compose 环境变量动态注入。容器间通信必须使用服务名(如 db)而非 localhost。
这次我也意识到,本地能跑 ≠ 部署环境也一定能跑,因为配置环境完全不同。
本地配置文件里是这样的:
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/java_gobang?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
而项目主配置里也同步补充了兼容参数:
yaml
spring:
profiles:
active: local
datasource:
url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
username: root
password: 1203xxxxxxRn.
部署时则通过 docker-compose.yml 的环境变量注入生产环境配置,而不是把部署地址直接写死在业务代码里。
这让我意识到一个很重要的规范:
- 本地开发配置和部署配置必须区分
- 数据源、端口、Profile 这些都应该交给配置管理
- 容器里访问数据库不能写
localhost,而要写服务名db - 能通过环境变量注入的配置,尽量不要写死在业务逻辑中
3. 数据库初始化规范
- 自动化初始化 :利用
docker-entrypoint-initdb.d实现首次启动自动建表。 - 测试账号前置 :在
db.sql中预置测试账号,配合前端login.html的 UI 提示,极大降低了面试官的体验门槛。
这次我把数据库初始化逻辑放进了db.sql,首次启动容器时就自动建库建表并插入测试数据。
sql
create database if not exists java_gobang;
use java_gobang;
drop table if exists user;
create table user(
userId int primary key auto_increment,
username varchar(50) unique,
password varchar(50),
score int,
totalCount int,
winCount int
);
insert into user values(null,'zhangsan','123',1000,0,0);
insert into user values(null,'lisi','123',1000,0,0);
insert into user values(null,'wangwu','123',1000,0,0);
insert into user values(null,'test','123456',1000,0,0);
这让我总结出一个很实际的规范:
- 初始化脚本适合放在容器第一次启动时执行
- 测试账号也应该放进初始化数据里,方便部署验证
- 但一旦挂载了数据卷,初始化脚本不会重复执行
- 如果想重新初始化数据库,必须清理旧卷再重建
这次我后面排查问题时,正是因为理解了这一点,才知道为什么改了db.sql后数据库没变化。
4. 页面入口与部署验证规范
login.html 这次不仅是页面文件,它实际上也承担了"部署完成后首个验证页面"的作用。
我在页面里保留了测试账号提示,方便部署后直接验证。
html
<div class="bg-white border-4 border-black p-3 text-sm font-bold shadow-neo-sm mb-2 text-center transform rotate-1 hover:rotate-0 transition-transform cursor-default">
<span class="text-lg">👋</span> 面试官您好!欢迎体验<br/>
可用测试账号:<span class="bg-black text-white px-2 py-0.5 mx-1 inline-block mt-1">test</span>密码:<span class="bg-black text-white px-2 py-0.5 mx-1 inline-block">123456</span>
</div>
从我的角度看,这其实也是一种部署规范:
- 部署后的首页应该清晰可访问
- 最好准备一个可直接验证的测试账号
- 这样能快速区分"页面打不开""接口异常""数据库未初始化"这几类问题
5. 部署记录与环境追踪
这次还多出了 .codebuddy 下的几个文件:
这些不属于业务代码,但它们记录了:当前项目对应的部署环境、预览地址、部署实例信息、沙箱映射关系。
从我的角度理解,这些文件的作用更像是: 自动化部署过程中的环境记录和追踪信息。它们帮助我知道项目被部署到了哪里、当前访问地址是什么,也能帮助后续排查部署问题。
三、实战突发:深刻的生产级问题排查
这次实践中我遇到的主要问题:
1. 本地能登录,部署后不能登录/注册
这是这次最核心的问题。一开始我以为是代码有 bug,但最后定位下来,发现不是业务逻辑错,而是部署环境的数据库连接方式和本地不一致。
容器里的后端在连接 MySQL 8 时出现了认证兼容问题,导致登录、注册请求虽然到了后端,但数据库连接失败。
最后定位到的问题是:
- JDBC 连接缺少
allowPublicKeyRetrieval=true - MySQL 8 的认证机制在容器环境下更严格
所以本地看起来没问题,线上却失败了。最后我通过给数据源 URL 增加这个参数解决了问题。
2. 改了 db.sql,服务器数据库却没更新
这是我这次第一次比较深刻地理解 Docker 数据卷。
一开始我以为:改了 db.sql,重新 docker-compose up,数据库就会重新执行 SQL。但实际上不是这样。
因为 MySQL 初始化脚本只会在数据目录为空时执行一次,而我又把数据库目录挂到了数据卷上,所以之前的数据一直还在。
也就是说:容器重启了,但数据库卷没删,因此不会重新初始化。
最后必须通过:
docker-compose down -v
删除旧卷,再重新启动,数据库才会按照新脚本重新初始化。
3. 本地环境和部署环境差异被放大
本地运行时,很多问题不容易暴露:
- 本地可能已经缓存过数据库连接状态
- 本地库账号和远程库账号不完全一样
- 本地用
localhost - Docker 内部网络必须用服务名
db
这次让我真正体会到: 部署问题很多时候不是代码错,而是环境错、配置错、初始化方式错。
4. 线上运行一段时间后,登录/注册突然卡顿报错(深度复盘)
这是我今天刚刚遇到的一个非常经典的"生产环境级"问题。
一开始项目部署上线后测试都正常,但过了一段时间,点击注册突然反应迟钝,最后直接抛出 500 错误。
现象:注册失败与"反应迟钝"
在一次部署后,发现前端注册登陆请求频繁报 500 错误,且响应极慢。
排查链路 (Debug Workflow)(五步法):
- 查容器状态 :我首先用
docker ps查看,发现java_gobang_db(MySQL容器)一直在不断重启(Restarting)。 - 查应用日志 :通过
docker logs --tail 100 java_gobang_app发现后端抛出 Communications link failure,但数据库服务显示 Up,应用抛出了数据库连接超时的异常。 - 查系统资源(关键) :进一步排查,执行
df -h查看服务器存储,惊讶地发现根目录/占用率竟然达到了 100%!惊人发现:服务器磁盘占用 100%! - 定位真凶:原来是因为我之前多次修改代码并重新部署,服务器上残留了大量无用的、带有时间戳的项目构建目录、悬挂镜像(Dangling Images)以及旧的运行日志,直接把云服务器的磁盘撑爆了。
- 导致结果 :由于频繁部署且未清理旧镜像,导致磁盘满额。MySQL 在磁盘满时无法写入
Binlog和临时文件,导致事务阻塞,从而引发前端请求超时报错。
彻底解决方案:
我没有选择单纯地重启,而是执行了一次彻底的清理和重装,以防止原有数据卷因为磁盘写满而损坏:
bash
# 1. 停止并强制删除旧容器及关联的数据卷
docker-compose down -v
# 2. 深度清理系统冗余(一键清理未使用的镜像、卷、网络、构建缓存)
docker system prune -a --volumes -f
# 3. 找到最新代码目录,重新构建并冷启动
docker-compose up -d --build
这个坑让我深刻明白,服务器运维也是部署的一部分。容器化虽然方便,但如果不注意及时清理构建垃圾,"牵一发而动全身",磁盘爆满会直接让最基本的业务逻辑(如登录注册)卡死。
四、 总结与展望
这次实践带给我的收获
经过这次实践,我觉得自己不只是"学会了 Docker 命令",而是对完整部署流程有了更实际的理解。
我学到的东西包括:
- 如何把 Java 项目写成 Dockerfile
- 如何用多阶段构建减少镜像体积
- 如何用 docker-compose 一次拉起多个服务
- 如何让应用容器和数据库容器互相通信
- 如何用初始化脚本自动建库建表
- 如何通过环境变量管理部署配置
- 如何区分本地配置和生产配置
- 如何通过日志和系统资源监控(df -h)定位部署错误
- 如何理解数据卷对数据库状态的持久化影响
对我来说,这次最大的收获不是"部署成功了",而是我第一次真正理解了:
项目上线不是把代码传上去就结束了,而是要把入口、配置、数据库、容器、资源监控、日志和验证链路全部串起来。
我对这次实践的整体评价
如果从我自己的角度总结,这次自动化 Docker 部署实践,已经不只是"试着跑一下容器",而是完成了一次比较完整的部署闭环:
- 有应用镜像构建
- 有服务编排
- 有数据库初始化
- 有默认访问入口
- 有环境配置区分
- 有部署记录
- 有生产级问题排查和修复(特别是处理磁盘爆满和数据卷损坏的实战)
虽然过程中遇到了数据库认证、数据卷初始化以及磁盘空间耗尽导致的服务宕机等问题,但正是这些问题让我真正理解了部署和本地运行的差别。
所以这次实践对我来说,最大的意义不是"写了几个 Docker 文件",而是第一次把项目的开发、运行、自动化部署、运维和验证串成了一个完整过程。
核心收获总结:
通过这次实践,我理解到:部署不是把代码传上去,而是要构建一个可监控、可复现、可快速修复的生命周期。
- 掌握了多阶段构建与容器编排的生产实践。
- 深刻理解了 Docker 数据卷(Volume)的持久化机制与初始化时机。
- 具备了通过系统日志和资源监控定位容器级故障的实战经验。
- 真正的技术实力不仅体现在编写优雅的逻辑,更体现在当系统在生产环境"趴窝"时,你能冷静地通过
logs和prune快速恢复服务的能力。