【项目】从“本地能跑”到“生产级部署”:Java + Docker 自动化部署深度复盘

从"本地能跑"到"生产级部署":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

一、这次实践我做了什么

  1. 用 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 镜像"。
  1. 用 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)(五步法):

  1. 查容器状态 :我首先用 docker ps 查看,发现 java_gobang_db(MySQL容器)一直在不断重启(Restarting)。
  2. 查应用日志 :通过 docker logs --tail 100 java_gobang_app 发现后端抛出 Communications link failure,但数据库服务显示 Up,应用抛出了数据库连接超时的异常。
  3. 查系统资源(关键) :进一步排查,执行 df -h 查看服务器存储,惊讶地发现根目录 / 占用率竟然达到了 100%!惊人发现:服务器磁盘占用 100%!
  4. 定位真凶:原来是因为我之前多次修改代码并重新部署,服务器上残留了大量无用的、带有时间戳的项目构建目录、悬挂镜像(Dangling Images)以及旧的运行日志,直接把云服务器的磁盘撑爆了。
  5. 导致结果 :由于频繁部署且未清理旧镜像,导致磁盘满额。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)的持久化机制与初始化时机。
  • 具备了通过系统日志和资源监控定位容器级故障的实战经验。
  • 真正的技术实力不仅体现在编写优雅的逻辑,更体现在当系统在生产环境"趴窝"时,你能冷静地通过 logsprune 快速恢复服务的能力。
相关推荐
qq_333120972 小时前
头歌答案--爬虫实战
java·前端·爬虫
摇滚侠2 小时前
JAVA 项目教程《苍穹外卖-11》,微信小程序项目,前后端分离,从开发到部署
java·开发语言·微信小程序
muls12 小时前
java面试宝典
java·linux·服务器·网络·算法·操作系统
执笔论英雄2 小时前
【vllm】vllm根据并发学习调度
java·学习·vllm
瑶总迷弟2 小时前
Python入门第6章:字典(键值对数据结构)
java·数据结构·python
o丁二黄o2 小时前
【MyBatisPlus】MyBatisPlus介绍与使用
java
_MyFavorite_2 小时前
JAVA重点基础、进阶知识及易错点总结(14)字节流 & 字符流
java·开发语言·python
zt1985q3 小时前
本地部署 Home Assistant 高级自动化 AppDaemon 并实现外部访问
运维·服务器·网络·网络协议·自动化
志栋智能3 小时前
轻量级部署:低成本实现混合云环境自动化巡检
运维·网络·人工智能·自动化