SpringBoot Docker镜像跨平台部署与Linux验收

发布时间:2026/10/29 0:58:50
SpringBoot Docker镜像跨平台部署与Linux验收

简介:这是一份面向 Java 后端开发者与运维初学者的 Docker 部署实践文档,围绕在 Windows 环境中将 SpringBoot 项目构建为镜像,并迁移到 Linux 主机运行展开。内容从 Dockerfile 基础写法切入,串联 jar 包复制、端口暴露、镜像构建、容器启动与日志查看,并延伸到容器网络连通性测试、宿主机或独立 MySQL 容器访问、数据库挂载目录持久化等常见问题。文档还涉及共享主机网络、ping 与 telnet 排查、Docker Compose 管理多容器应用等排错思路,对镜像命名、端口冲突、容器重启策略、MySQL 初始化密码与目录映射等细节也有交代,帮助读者理解镜像、容器与数据卷之间的关系,形成从本地开发机到 Linux 部署环境的完整参考。文件包共 1 个 docx,约 2.48MB,以图文说明和命令示例为主,便于对照实践。已有 135 人学习,适合作为 SpringBoot 容器化入门与跨平台部署的实操资料。

1. Windows 里把镜像跑起来只是前半场,搬到 Linux 才是真正的验收

在 Windows 上把 SpringBoot 项目打成 Docker 镜像、docker run起容器、浏览器打开 8080 看到首页,很多人到这一步就收工了;等docker save出一个几百兆的 tar 丢到 Linux 上docker load起来,容器启动几秒就退出,日志里一行Connection refused指向数据库,才发现这套环境从来不是“装个 Docker”这么简单。Dockerfile 里 jar 路径写错,容器网络默认桥接解析不了容器名,MySQL 数据目录挂载权限对不上,Linux 上的 docker-ce 版本比镜像还老,任何一处都能让页面白屏。这套流程适合手上有 SpringBoot + MySQL + Redis、想在 Windows 上开发调试、再把同一份镜像搬到 Linux 跑起来的人。下面按装什么、怎么装、坏了怎么救走一遍。

2. Dockerfile 落地:openjdk:8-jdk-alpine 五行写死一个可移植镜像

镜像能不能在另一台机器上活着跑起来,八成取决于 Dockerfile 里那几个路径和基础镜像选得对不对。这一章先把项目结构钉死,再逐行拆一份能直接抄的 Dockerfile,最后把docker build和docker run两刀砍下去。

2.1 先把 jar 包位置和 Dockerfile 摆放位置固定下来

SpringBoot 项目打包用 Maven 就是一条命令,产出的 jar 默认落在target目录下,名字带版本号。真正容易翻车的是位置:Dockerfile必须放在项目根目录,也就是pom.xml同级,因为它决定了docker build的构建上下文(build context);COPY后面的源路径永远是相对这个上下文算的,不是相对Dockerfile所在行的位置。

springboot-demo/ ├── pom.xml ├── Dockerfile <- 必须在这一层 ├── src/ │ └── main/java/... └── target/ └── demo-app-1.0.0.jar <- mvn package 之后才有

打包命令用跳过测试的那条,本地验证过功能再跳,别在 CI 里也一路跳过:

# 清理旧产物并跳过测试打包,产物落在 target/ 下 mvn clean package -DskipTests

打包完先ls target/*.jar看一眼真实文件名。target目录里经常还有xxx.jar.original这种后缀,COPY target/*.jar app.jar在某些情况下会匹配到多个文件直接报错,所以我在正式 Dockerfile 里一律写死完整文件名,宁可改一次 Dockerfile 也不留这个隐患。如果你的项目名和版本号经常变,那就退一步用COPY target/*.jar前先确认target下只有一个 jar,或者加.dockerignore把*.original排除掉。

2.2 逐行拆 Dockerfile:FROM、WORKDIR、COPY、EXPOSE、ENTRYPOINT

这是能直接用的版本,比原始写法多了一个时区参数,原因后面讲:

# 基础镜像用 OpenJDK 8 的 alpine 精简版,体积小,够跑 SpringBoot FROM openjdk:8-jdk-alpine # 容器内的工作目录,后面的相对路径都基于它 WORKDIR /app # 把本地构建出的 jar 复制进镜像,并重命名为固定的 app.jar COPY target/demo-app-1.0.0.jar app.jar # 声明应用监听 8080,属于元数据,不做端口映射 EXPOSE 8080 # 启动命令,exec 形式,PID 1 就是 java 进程 ENTRYPOINT ["java", "-Duser.timezone=Asia/Shanghai", "-jar", "/app/app.jar"]

FROM选openjdk:8-jdk-alpine的理由很实际:SpringBoot 2.x 配 JDK 8 是主流组合,alpine 版本镜像只有百来兆,推来推去不心疼。要注意 alpine 里没有 glibc,项目如果依赖 netty-tcnative 或者某些本地库,运行时会报Error loading shared library,这种项目就得换openjdk:8-jdk-slim这类基于 Debian 的镜像。

WORKDIR /app不只是切目录,它会在镜像层里创建这个目录,后面的COPY和ENTRYPOINT里写/app/app.jar才对得上。COPY的两个参数顺序是“源 目标”,源必须在构建上下文里,../target/xxx.jar这种写法直接失败,因为上下文之外的文件 Docker 看不见。

EXPOSE 8080只是声明,不 publish,真正对外还得靠docker run -p。写它的价值在于别人docker inspect镜像时能看到这个服务该用哪个端口。

ENTRYPOINT用 JSON 数组的 exec 形式,不是ENTRYPOINT java -jar app.jar这种 shell 形式。区别很致命:shell 形式下 PID 1 是/bin/sh -c,java 进程是子进程,docker stop发的 SIGTERM 到不了 JVM,容器只能等 10 秒超时后被 SIGKILL 强杀,SpringBoot 的优雅停机、连接池关闭全都不执行。这个坑在开发环境看不出来,上了 Linux 跑久了才会发现每次重启都丢在途请求。

时区参数-Duser.timezone=Asia/Shanghai是我必加的。alpine 镜像默认 UTC,容器里new Date()出来的时间比北京时间少 8 小时,日志时间戳全是错的,排查问题时能把人绕晕。alpine 里没装 tzdata,所以配TZ环境变量没用,直接给 JVM 传时区最省事。

2.3 docker build 后面那个点,以及镜像命名和缓存

构建命令在 PowerShell 里执行,先cd到含Dockerfile的项目根目录:

# -t 指定镜像名和标签,最后那个点代表当前目录作为构建上下文 docker build -t demo-app:1.0.0 .

那个.是最容易被忽略又最容易出事的地方。它把当前目录整个打包发给 Docker daemon,target里几百兆的 jar、.git目录、node_modules都会被送进去,构建慢得像卡住。常见做法是在项目根目录放一个.dockerignore:

.git .idea *.iml target/*.original logs/

注意别把target/整个排除掉,jar 就在里面,COPY会直接失败。

-t demo-app:1.0.0的命名建议固定成“项目名:版本号”,后面在 Linux 上docker save、docker load、docker run全程用这个名字,版本号跟着pom.xml走。见过有人用docker build -t app .,结果本地攒了七八个同名不同内容的app:latest,出问题时根本分不清跑的是哪个包,这就是自找的。

缓存问题也得提一句。COPY target/xxx.jar app.jar这一行只要 jar 内容变了,缓存就失效,后面的层全部重建,这是正常的。但如果改完代码忘了mvn package,COPY拿到的还是旧 jar,构建依然“成功”,容器里的代码却是老的,排查半天以为是环境问题。我一般把打包和构建串成一条命令:

mvn clean package -DskipTests; if ($?) { docker build -t demo-app:1.0.0 . }

if ($?)是 PowerShell 里判断上一条命令是否成功,打包失败就不构建,省得拿着旧包骗自己。

2.4 起容器、看日志、确认端口映射

# -d 后台运行,-p 宿主机端口:容器端口,--name 给容器起个固定名字 docker run -d -p 8080:8080 --name demo-app demo-app:1.0.0 # 看容器是否还活着,STATUS 里出现 Up 才算数 docker ps # 跟踪日志,出问题第一眼看这里 docker logs -f demo-app

-p 8080:8080左边是 Windows 宿主机端口,右边是容器内端口,右边必须和 Dockerfile 里EXPOSE以及 SpringBoot 的server.port一致。左边被占用是最常见的启动失败原因,比如本机已经跑了一个 Tomcat 或者另一个 SpringBoot,docker run会直接报port is already allocated,改左边的端口就行,比如-p 8081:8080。

docker logs是这套环境的黑匣子。SpringBoot 启动失败都会在日志里留下痕迹:UnknownHostException指向数据库地址解析不了,Access denied for user指向账号密码或授权范围不对,Communications link failure指向网络不通。容器刚起来就退出的时候docker ps看不到它,得用docker ps -a找,再docker logs看退出原因。

浏览器访问http://localhost:8080能出页面,说明 Dockerfile、构建、端口映射这三段路是通的。但页面能出来不代表业务能跑,只要涉及数据库查询,下一步就得处理容器怎么连到 MySQL。

3. 容器网络与中间件:dev_net、MySQL 8.0.24 和 Redis 怎么串起来

Windows 上跑容器访问本机数据库,是这套流程里最容易“看起来通了其实没通”的一段。原文里试过--net=host,也试过先 ping 再改配置,我把顺序重新理一遍,顺手把 MySQL 和 Redis 也容器化。

3.1 默认 bridge 解析不了容器名,dev_net 才行

Docker 容器默认挂在bridge网络上,这个默认网络只有 IP 转发,没有内嵌 DNS,容器之间用容器名互相访问会直接报UnknownHostException。自己docker network create出来的用户自定义网络才带 DNS 解析,容器名可以直接当主机名用。

# 创建一个自定义桥接网络,后面的应用、MySQL、Redis 都接进来 docker network create dev_net # 看网络列表,确认 dev_net 在里面 docker network ls

接入用docker network connect,容器跑起来之后也能接,不用重建:

docker network connect dev_net mysqldoc docker network connect dev_net redis docker network connect dev_net demo-app

应用容器这一步经常被漏掉。MySQL 和 Redis 都连上dev_net了,应用还在默认bridge上,照样解析不了mysqldoc。三条connect一条都不能少,接了之后用docker inspect验一下:

# 只取容器在 dev_net 里的 IP,有输出说明接上了 docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{$v.IPAddress}}{{end}}' demo-app

3.2 从容器里 ping 和 telnet 3306,先判断再改配置

连不上数据库时别急着改 yml,先在容器里做两步诊断。原始做法是从应用容器 ping 数据库容器或 IP,再 telnet 3306 端口。

# 容器内 ping 数据库容器名,验证 DNS 解析和二层连通 docker exec -it demo-app ping mysqldoc # 容器内 telnet 数据库端口,验证 TCP 层通不通 docker exec -it demo-app telnet mysqldoc 3306

这里有个现实问题:openjdk:8-jdk-alpine镜像里既没有ping也没有telnet,直接执行会报not found。常见做法是临时进容器装一个 busybox 扩展,或者反过来从 MySQL 容器往应用容器 ping,因为mysql:8.0.24镜像里通常有ping。更省事的做法是:

# 用自带 bash 的容器做网络探测,把它接到同一个网络里 docker run --rm --net dev_net busybox ping -c 2 mysqldoc

--rm表示跑完即删,不留垃圾容器;--net dev_net让它和业务容器在同一个网络里,这样测出来的结果才有参考价值。ping 通只说明网络层没问题,3306 还得单独验,端口没监听或者 MySQL 没起来,ping 是通但 telnet 不通的。

3.3 MySQL 8.0.24 容器化:四个挂载目录和 3307 端口

Windows 上先建挂载目录,PowerShell 里一条mkdir可以带多个路径:

mkdir D:\development_sources\docker\mysql\log,D:\development_sources\docker\mysql\data,D:\development_sources\docker\mysql\conf,D:\development_sources\docker\mysql\mysql-files

然后跑容器,参数一个一个摆开:

docker run -p 3307:3306 --restart=always --name mysqldoc ` -v D:\development_sources\docker\mysql\log:/var/log/mysql ` -v D:\development_sources\docker\mysql\data:/var/lib/mysql ` -v D:\development_sources\docker\mysql\conf:/etc/mysql ` -v D:\development_sources\docker\mysql\mysql-files:/var/lib/mysql-files ` -e MYSQL_ROOT_PASSWORD=root ` -d mysql:8.0.24

PowerShell 里的续行符是反引号`,别抄成 Linux 的反斜杠\,抄错了命令会被拆成几条执行,报一堆莫名其妙的错。

参数作用取舍说明
-p 3307:3306宿主机 3307 映射容器 3306本机已有 MySQL 占了 3306,开发机这样错开最省事
--restart=always容器异常退出或 Docker 重启后自动拉起开发机也建议加,省得每次开机手动 start
--name mysqldoc固定容器名后面容器名要写进 yml,名字不能乱改
-v .../log:/var/log/mysql日志落宿主机出问题直接在 Windows 上看日志文件
-v .../data:/var/lib/mysql数据文件落宿主机容器删了数据还在,这条是命根子
-v .../conf:/etc/mysql配置文件挂进去想改my.cnf不用进容器
-v .../mysql-files:/var/lib/mysql-files导入导出目录LOAD DATA之类操作要用
-e MYSQL_ROOT_PASSWORD=rootroot 初始密码只在数据目录为空、首次初始化时生效

最后一条特别容易误解:MYSQL_ROOT_PASSWORD只在/var/lib/mysql为空、容器执行初始化时读取。挂载目录里已经有数据了,改这个环境变量密码不会变,得进容器用ALTER USER改。

容器起来后用数据库客户端连127.0.0.1:3307验证,注意端口是 3307 不是 3306。业务账号别直接用 root,在容器里建一个限定来源的账号:

# 进入容器内的 mysql 客户端 docker exec -it mysqldoc mysql -uroot -p
-- 应用账号只允许从任意容器来源连接,密码自己换掉 CREATE USER 'app_user'@'%' IDENTIFIED BY 'App@123456'; GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'%'; FLUSH PRIVILEGES; -- 如果只允许某个固定 IP 来源,把 % 换成具体地址 -- CREATE USER 'app_user'@'192.168.0.27' IDENTIFIED BY 'App@123456';

MySQL 8 的默认认证插件是caching_sha2_password,老的 JDBC 驱动连它会报Public Key Retrieval is not allowed,所以后面 JDBC URL 里要带allowPublicKeyRetrieval=true,或者把账号改成mysql_native_password。这两条路选一条,别两边都改完再互相干扰。

3.4 Redis 容器:6380 端口、AOF 和密码

Redis 的部署结构简单得多,一个数据目录就够:

mkdir D:\development_sources\docker\redis\data docker run -p 6380:6379 --restart=always --name redis ` -v D:\development_sources\docker\redis\data:/data ` -d redis redis-server --appendonly yes --requirepass redis

-p 6380:6379同样是为了避开宿主机已经占用的 6379。--appendonly yes打开 AOF 持久化,每条写命令追加到文件,容器重启后数据能重建;不打开的话重启即清空,开发环境丢点缓存无所谓,但如果拿 Redis 存了业务状态就会出事。--requirepass redis设密码,最后这几个参数是传给redis-server的命令行参数,顺序必须在镜像名之后,写到镜像名前面会被 Docker 当成自己的参数而报错。

容器起来后docker network connect dev_net redis接进同一个网络,再用 Redis 桌面客户端连127.0.0.1:6380,密码填redis,能PING出PONG就算通了。

3.5 yml 里写容器名还是 IP,以及 --net=host 在 Windows 上的局限

网络通了之后改 SpringBoot 的配置文件,host直接写容器名:

spring: datasource: url: jdbc:mysql://mysqldoc:3306/app_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: app_user password: App@123456 redis: host: redis port: 6379 password: redis

两个端口别写错:容器内 MySQL 永远是 3306,Redis 永远是 6379,3307和6380只是宿主机映射出去给 Windows 上的客户端用的。在 yml 里写成 3307,容器之间是连不上的,这个错我见过不止一次。

serverTimezone=Asia/Shanghai和前面 JVM 的时区参数是一套的,两边都不写,插入的时间字段就会差 8 小时。useSSL=false在开发环境关掉省事,生产环境该开还得开。

至于--net=host,原文试过之后评价“意义不大”,我补一下原因:Windows 上的 Docker Desktop 跑在一层轻量虚拟机里,--net=host共享的是那台虚拟机的网络栈,不是 Windows 宿主机本身,所以想靠它连 Windows 上的 MySQL,大概率还是不通。Linux 上--net=host是真共享宿主机网络,行为完全不同。跨平台部署时这个差异足够让人怀疑人生,所以我的做法是统一走dev_net加容器名,Windows 和 Linux 行为一致,迁移的时候不用改配置。

4. 避坑与排查:容器起不来、连不上库的五个现场

下面五条都是我自己踩过或者帮别人收拾过的现场,按“现象 → 原因 → 解决”记。

4.1 构建和启动阶段的三条

第一条,容器启动几秒就 Exited,docker ps里看不到。

现象:docker run回车后返回一串容器 ID,docker ps空空如也,docker ps -a能看到状态是Exited (1)。

原因:九成是 jar 路径不对或者 jar 本身没打出来。COPY在构建阶段就会失败,但如果是ENTRYPOINT里的路径写错,构建能过,运行时 java 找不到 jar 直接退出。

解决:先docker logs <容器名>看最后几行,典型报错是Error: Unable to access jarfile /app/app.jar。回到项目里ls target/*.jar确认文件名,再核对COPY的目标名和ENTRYPOINT里的是否一致;顺便确认mvn package真的执行过,别拿着空target目录构建。

第二条,改了代码,镜像重建了,容器里跑的还是老逻辑。

现象:接口返回的字段和本地代码对不上,日志里的时间戳也停在几小时前。

原因:docker build用了缓存,或者更常见的是打包那步漏了,COPY复制进去的就是旧 jar。Docker 只按文件内容算哈希,内容没变就复用缓存层,它不会知道你改的是src里的 java 文件。

解决:养成mvn clean package再docker build的固定顺序,怀疑缓存问题时加--no-cache:

docker build --no-cache -t demo-app:1.0.0 .

--no-cache会把所有层重建,慢但干净。另外docker run前先docker rm -f demo-app删旧容器,不然--name冲突会直接报错,有人以为报错就是构建失败,其实只是名字被占了。

第三条,docker run报端口已被占用。

现象:Bind for 0.0.0.0:8080 failed: port is already allocated。

原因:宿主机 8080 已经被别的进程或容器占着,Windows 上常见的是另一份 SpringBoot 在 IDEA 里跑着没关,或者上次的容器没删干净。

解决:netstat -ano | findstr :8080找到占用进程,该关的关掉;不想关就换映射端口,-p 8081:8080,容器内还是 8080,对外访问改成 8081 即可。别去改 Dockerfile 里的EXPOSE,那是元数据,改了也解决不了冲突。

4.2 连库和迁移阶段的两条

第四条,容器里访问数据库报UnknownHostException: mysqldoc。

现象:应用启动日志里抛java.net.UnknownHostException,或者Communications link failure,但用数据库客户端从 Windows 连127.0.0.1:3307是好的。

原因:应用容器和 MySQL 容器不在同一个用户自定义网络上,默认bridge没有 DNS 解析,容器名根本不是一个能解析的主机名。

解决:确认三方都接进了dev_net,docker network inspect dev_net看Containers里是不是三个容器都在。少哪个就docker network connect dev_net <名字>补上,然后重启应用容器让 SpringBoot 重新解析。还有一种情况是 yml 里写了容器的 IP,容器重建后 IP 变了,所以写容器名、不写 IP。

第五条,数据库容器升级或者换机器后,数据“没了”。

现象:在 Windows 上跑得好好的库,换到 Linux 上重新docker run之后表全空,只剩一个空库。

原因:-v挂的是宿主机目录,换机器等于换了一个空目录,MySQL 发现/var/lib/mysql为空就重新初始化。容器本身不会随镜像带走数据,这点和镜像完全两回事。

解决:迁移前先mysqldump导出 SQL,在新机器上起容器、建库、导数据,不要指望把 Windows 的数据目录直接拷到 Linux 上能用——目录里的文件属主是容器内的 mysql 用户(uid 通常是 999),Linux 上权限对不上,容器会起不来,日志里一堆Permission denied。如果非要拷目录,得在新机器上chown -R 999:999对齐 uid,还得处理 SELinux 上下文,不如导出导入干净。

5. 迁移到 Linux:docker save 打包 + CentOS 7 装 docker-ce-18.03.1.ce

Windows 那边验证通过了,接下来的活是把镜像搬到 Linux。核心就三件事:docker save出 tar、目标机装 Docker、docker load回来重跑。

5.1 docker save 三个 tar:应用、MySQL、Redis

镜像必须一个一个存,docker save一条命令只打包一个镜像(新版本可以带多个镜像名,但老版本不行,别赌):

# -o 指定输出文件,后面跟镜像名:标签 docker save -o demo-app.tar demo-app:1.0.0 docker save -o mysqlimage.tar mysql:8.0.24 docker save -o myredisimage.tar redis

三个 tar 都要存,因为 Linux 上的 Docker 不保证能拉到完全一样的镜像,尤其是没有外网或者镜像站抽风的时候。文件大小心里要有数:MySQL 8.0.24 的镜像 tar 通常几百兆,应用镜像看依赖,几十兆到几百兆。三个加起来超过 1G 很正常,传输前先看目标盘的剩余空间。

镜像名和标签要和 Linux 上docker run用的完全一致,save的时候写mysql:8.0.24,load回来就还是mysql:8.0.24;写mysql不带标签,load 回来就是mysql:latest,后面启动脚本里的标签对不上,容器起不来。这个细节我吃过亏,现在 save 之前都先docker images抄一遍完整名字。

还有一个跨架构的注意点:Windows 上的 Docker Desktop 默认拉的是linux/amd64架构的镜像,和 x86_64 的 CentOS 7 一致,正常不会出问题。如果构建机是 ARM 架构,就得在docker build时显式指定--platform linux/amd64,否则 load 到 Linux 上会报no matching manifest或者容器起来直接exec format error。

5.2 CentOS 7 上装 docker-ce-18.03.1.ce:源、依赖、启动

Linux 侧先确认版本,内核 3.10 的 CentOS 7 是这套组合里最常见的底子:

uname -r # 3.10.0-1062.56.1.el7.x86_64

远程连接工具随便挑一个顺手的 SSH 客户端,输入 IP、端口、账号密码连上,命令行和文件管理都在一个界面里,后面传 tar 包方便。开发环境为了省事经常直接关防火墙,生产环境千万别这么干:

# 关闭防火墙并禁用开机自启,仅限内网开发环境 systemctl stop firewalld systemctl disable firewalld

装 Docker 之前先把旧版本清干净,CentOS 7 上有些老包会互相干扰:

# 卸载可能存在的旧版本,没装过会提示 nothing to do,正常 yum remove docker docker-common docker-selinux docker-engine # yum-utils 提供 yum-config-manager,另外两个是存储驱动依赖 yum install -y yum-utils device-mapper-persistent-data lvm2 # 添加 docker-ce 的 yum 源,地址换成你环境能访问的镜像站 yum-config-manager --add-repo http://<你的镜像站地址>/docker-ce/linux/centos/docker-ce.repo # 列出可安装版本,从高到低排序 yum list docker-ce --showduplicates | sort -r # 安装指定版本,这是我们这套环境验证过的版本 yum -y install docker-ce-18.03.1.ce

版本这里要留意 yum 包名的完整形式,实际的包名通常是docker-ce-18.03.1.ce-1.el7.centos,yum install docker-ce-18.03.1.ce能匹配到就可以;匹配不到就用yum list输出里的全名。

18.03 这个版本比较老,好处是和旧内核、旧镜像兼容性好,坏处是有些新镜像的 manifest 格式它不认。如果docker load时报格式相关的错,不要硬扛,记下报错信息,确认镜像能在更新的 Docker 上 load 成功再决定要不要升版本。

装完启动并设开机自启:

systemctl start docker systemctl enable docker # 确认版本,Client 和 Server 两段都要有输出 docker version

CentOS 7 上默认存储驱动可能是devicemapper,性能和稳定性都不如overlay2。内核 3.10 是支持 overlay2 的,建议显式写进配置:

{ "storage-driver": "overlay2", "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }

把这段写进/etc/docker/daemon.json,然后systemctl restart docker。日志限制那两行是后悔药:不加的话容器日志会把磁盘写满,等到no space left on device的时候,服务已经挂了。改完存储驱动如果起不来,先docker info | grep -i storage看实际生效的驱动,必要时把/var/lib/docker清空重来——前提是没有重要镜像,所以这一步要在 load 之前做。

5.3 上传 tar 包、load 镜像、按 Linux 的路径重跑容器

用 SSH 客户端的文件管理把三个 tar 传到 Linux 的自定义目录,比如/opt/loadimages,然后逐个加载:

cd /opt/loadimages # -i 指定输入文件,执行完 docker images 能看到镜像 docker load -i demo-app.tar docker load -i mysqlimage.tar docker load -i myredisimage.tar # 确认三个镜像都在,标签对得上 docker images

docker load可能输出Loaded image: demo-app:1.0.0,也可能只输出层 ID 的进度,最后以docker images的结果为准。

容器要按 Linux 的路径重跑,不能照抄 Windows 的D:\...。先建目录:

mkdir -p /data/docker/mysql/{log,data,conf,mysql-files} mkdir -p /data/docker/redis/data

MySQL 和 Redis 的启动命令除了挂载路径,其余参数和 Windows 上保持一致:

docker network create dev_net docker run -p 3307:3306 --restart=always --name mysqldoc \ -v /data/docker/mysql/log:/var/log/mysql \ -v /data/docker/mysql/data:/var/lib/mysql \ -v /data/docker/mysql/conf:/etc/mysql \ -v /data/docker/mysql/mysql-files:/var/lib/mysql-files \ -e MYSQL_ROOT_PASSWORD=root \ -d mysql:8.0.24 docker run -p 6380:6379 --restart=always --name redis \ -v /data/docker/redis/data:/data \ -d redis redis-server --appendonly yes --requirepass redis docker run -d -p 8080:8080 --name demo-app demo-app:1.0.0 docker network connect dev_net mysqldoc docker network connect dev_net redis docker network connect dev_net demo-app

顺序上先把中间件起好再起应用,不然应用启动时连不上数据库,SpringBoot 可能直接启动失败。数据从 Windows 迁过来的话,用mysqldump导出的 SQL 在新容器里source一遍,别直接拷数据目录。

5.4 SELinux 和目录权限:CentOS 上特有的两个坑

CentOS 7 默认开着 SELinux,-v挂载宿主机目录时容器进程可能没有写权限,表现是 MySQL 容器启动失败,日志里Can't create/write to file或者Permission denied。开发环境图省事可以临时关掉:

# 查看当前状态,Enforcing 表示开着 getenforce # 临时切成宽松模式,重启后恢复 setenforce 0

更规范的做法是在挂载参数后面加:z或:Z,让 Docker 自动打上 SELinux 标签,比如-v /data/docker/mysql/data:/var/lib/mysql:z。两种做法选一种,别同时上,否则排查时根本分不清是哪个改动起的作用。

权限本身也要看一眼,/data/docker/mysql/data如果被 root 提前占成 700,容器内的 mysql 用户写不进去,同样起不来。chown -R 999:999 /data/docker/mysql/data把属主对齐到容器内的 mysql uid,这是mysql:8.0.24镜像里的固定值。

6. 验收:load 之后用三条命令确认容器真的在跑

最后这步是我现在每次迁移必走的,几分钟就能把“看起来起来了”和“真的能对外服务”区分开。

第一步看容器状态,三个容器都得是Up,重启次数是 0:

docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

第二步验端口和服务。从 Linux 本机curl应用首页,curl不通就先看容器日志,别急着改配置:

# 应用返回 200 或者一段 HTML 才算数 curl -I http://127.0.0.1:8080 # 日志只取最后 50 行,重点找 Started Application 和异常堆栈 docker logs --tail 50 demo-app # 从应用容器反向验证数据库端口,能连上会打印 Open 或空行后挂住 docker exec -it demo-app sh -c "nc -zv mysqldoc 3306"

nc在 alpine 里通常可用,不行就用ping加docker exec -it mysqldoc mysql -uroot -p -e "select 1"从数据库侧验证。

第三步是最容易被跳过、也最能暴露问题的一步:跑一次真实的读写。打开一个涉及数据库查询的页面,或者调一个写接口,然后去数据库里select一下确认数据落库了;再往 Redis 里写一个 key,用客户端查出来。只验证首页能打开是不够的,首页往往是静态的,连接池根本没被触发。

验证项命令或动作通过标准
容器存活docker ps三个容器 Up,重启次数 0
应用可达curl -I http://127.0.0.1:8080返回 200 或 3xx
数据库连通页面触发查询返回数据,无Connection refused
数据落盘写接口后查库数据能查到,容器重启后还在
Redis 连通写 key 后用客户端读读得到值,PING返回PONG
开机自启重启机器后docker ps三个容器自动起来

那次在 CentOS 上 load 完镜像、docker ps三个容器全是 Up、首页也能打开,我直接跟人说好了,结果业务同事一点查询就报错——yml里还写着 Windows 上那个127.0.0.1:3307,容器里根本没有这个地址。从那以后我每次迁移完都强制走一遍“容器状态 → 端口连通 → 真实读写”这套动作,一步都不跳。希望这套流程能帮到你,少在黑匣子日志里绕几圈。

本文还有配套的精品资源,点击获取