ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot 容器镜像:Windows 构建到 Linux 运行全记录

Spring Boot 容器镜像:Windows 构建到 Linux 运行全记录

简介:面向需要将SpringBoot项目容器化的Java开发者和运维人员,详细讲解从Windows环境构建Docker镜像,再到Linux环境部署的完整流程。资源共包含1个docx文档,大小2.48MB,内容覆盖Dockerfile基础、镜像构建、容器运行、端口映射、网络共享模式、MySQL数据库容器化以及镜像打包迁移等知识点。文档先介绍在Java项目根目录创建Dockerfile并编写基础指令,接着通过docker build与docker run完成镜像构建和容器启动,并给出日志查看与访问验证方法。针对数据库连接问题,作者提供了共享主机网络、容器ping测试、数据库用户授权等解决方案,同时演示了MySQL容器化部署的挂载目录与参数配置。最后讲解如何将镜像从Windows打包传输到Linux环境直接运行,实现跨平台快速交付。已有134人学习下载,适合需要快速上手Docker化部署的初中级后端开发与运维工程师。

1. Windows 上构建 Spring Boot 镜像,搬到 Linux 运行:这套流程到底卡在哪

把 Spring Boot 项目打包成 Docker 镜像,在 Windows 上构建、再搬到 Linux 运行,是很多中小团队开发和部署服务器分离时的标准操作。Docker 的镜像跨平台特性让这个过程听起来很简单,但实际走一遍你会发现:Dockerfile 写错、端口映射冲突、容器里连不上数据库、镜像 save 之后 load 下来没有 tag,每一个问题都能让人耗掉一下午。

这篇文章是我把一套 Spring Boot 项目连带着 MySQL、Redis 一起容器化,再从 Windows 搬到 Linux 的全过程记录。重点不是科普概念,而是把命令参数、网络配置、数据卷挂载和常见报错修法写清楚。如果你正卡在“本地镜像能跑,上了服务器就挂”的阶段,照着下面顺序走完,应该能少踩几条弯路。

2. 先用 Dockerfile 把 Spring Boot 项目打成镜像:FROM、COPY、ENTRYPOINT 是关键

2.1 编写 Dockerfile:基础镜像选型、工作目录和 jar 包复制

Spring Boot 项目要容器化,第一步不是直接敲docker run,而是先准备一个 Dockerfile。这个文件没有扩展名,放在项目根目录下。我一般会先mvn clean package -DskipTests打好 jar 包,确认target目录里生成了可执行 jar,再去写 Dockerfile。

# 使用 OpenJDK 8 的 Alpine 精简版,镜像体积小,启动快 FROM openjdk:8-jdk-alpine # 容器内统一使用 /app 作为工作目录,后续命令都在这里执行 WORKDIR /app # 将本地 target 目录下的 jar 包复制进镜像,并重命名为 app.jar COPY target/demo-0.0.1-SNAPSHOT.jar app.jar # 声明容器对外服务端口为 8080,这个只是声明,不直接生效 EXPOSE 8080 # 容器启动后运行的命令,必须是 exec 格式,字符串数组 ENTRYPOINT ["java", "-jar", "/app/app.jar"]

FROM决定了基础镜像,带-alpine的版本比较小,适合部署在 Linux 服务器上。WORKDIR /app设置了当前目录,后面COPY的第二个参数app.jar是相对路径,实际会放到/app/app.jar。COPY的源路径是相对于 Dockerfile 上下文路径,也就是你执行docker build时后面那个点号所指的目录,所以 jar 包必须能在target/下找到,文件名也要对应。EXPOSE更像文档,真正让外部访问到容器端口,要靠后面docker run -p。ENTRYPOINT这里必须写成 JSON 数组形式,也就是带方括号,否则容器启动时可能因为命令格式问题直接退出。

这里有个容易忽略的点:jar 包文件名里尽量不要带中文或特殊字符,虽然 Windows 上能构建,但镜像传到 Linux 后,文件名解析偶尔会出乱子。项目如果是 Java 11 以上,可以把基础镜像换成openjdk:11-jdk-slim,启动命令和端口声明不用改。

2.2 构建镜像:docker build 的 -t 参数和末尾的上下文路径

Dockerfile 准备好后,打开 PowerShell,把当前目录切到项目根目录,也就是 Dockerfile 所在目录。执行构建命令前,最好先确认 Docker Desktop 已启动,否则会提示连不上 daemon。

docker build -t springboot-demo:1.0.0 .

-t的作用是给镜像起名并打 tag,springboot-demo:1.0.0中冒号前面是镜像名,后面是版本号。最后那个.是构建上下文路径,代表当前目录。Docker daemon 会把当前目录所有内容都接收过去作为上下文,Dockerfile 里的COPY target/demo-0.0.1-SNAPSHOT.jar才能找到文件。这个点号不是装饰,漏掉它,命令会直接报“Dockerfile 未指定”。

如果 Dockerfile 文件名不是默认的Dockerfile,比如叫Dockerfile-dev,那就需要加-f Dockerfile-dev指定。我平时会在项目里加一个.dockerignore文件,把.git、.idea、target/*这些不需要打包进镜像的东西排除掉,这样构建上下文体积小,构建速度快很多。

.git .idea target/*

这个文件内容不是给镜像用的,是让构建时不要往 daemon 传无关文件。构建完成后,用docker images能看到刚才的镜像和大小。如果构建中间报错,先看是下载基础镜像超时,还是 COPY 找不到 jar 包,这两类问题占大多数。

2.3 运行容器:-d、-p、--name 的配合

镜像构建好后,就可以用它创建容器了。第一次跑的时候,我默认不加--net之类的参数,先用最干净的配置把应用拉起来,确认基础启动没问题,再处理网络。

docker run -d -p 8080:8080 --name springboot-demo-app springboot-demo:1.0.0

-d表示后台运行,跑完命令后终端不会卡住。-p 8080:8080左边是宿主机端口,右边是容器内部端口,外部访问localhost:8080会进入容器内的 8080。如果宿主机 8080 已经被其他服务占用,可以改成8081:8080,这个映射值不是死的。--name给容器起一个固定名字,方便后续docker stop、docker logs引用。

启动后,用docker ps看容器状态。STATUS是Up说明还在运行,如果过几秒变Exited,那就是启动失败。查看日志是最直接的排查方法:

docker logs -f springboot-demo-app

-f是持续跟踪日志输出,看到Started DemoApplication之类的字样,说明 Spring Boot 已经启动完成。如果日志末尾报数据库连接拒绝,别急着搜数据库问题,先想一下 jar 包里的application.yml配置的是哪个地址。如果你本机跑了一个 MySQL,而容器默认是 bridge 网络,它在容器里访问localhost指向的是容器自己,不是宿主机,这也是下一章要解决的问题。

后面如果改了代码或配置,流程需要重来一遍:先重新打包 jar,再重新构建镜像,然后停止旧容器并删除,最后用新镜像创建新容器。直接改容器里的文件是没用的,容器一旦重建就会还原。

3. 容器里的 Spring Boot 怎么访问数据库:网络模式、ping 排查与 yml 配置

3.1 先确认网络通不通:docker exec 进容器执行 ping 和 telnet

Spring Boot 容器默认跑在 bridge 网络,它有一个独立的网段。如果数据库跑在宿主机上,或者数据库在另一个容器里,直接访问默认配置的 IP 很可能不通。我遇到这种情况,第一反应不是改配置,而是先进容器里做一次网络连通性验证。

docker exec -it springboot-demo-app ping 192.168.1.100 docker exec -it springboot-demo-app telnet 192.168.1.100 3306

docker exec -it是进入运行中容器的交互模式,后面接容器名和要执行的命令。第一条命令用 ping 测主机可达性,第二条用 telnet 测 3306 端口是否能连通。如果 ping 不通,说明容器和数据库不在同一个可路由的网络里;如果 ping 通但 telnet 不通,说明端口被防火墙挡住,或者 MySQL 没在监听。命令里的192.168.1.100要换成你数据库的实际 IP。

如果在容器里没有 ping 或 telnet 命令,那是因为基础镜像太精简,可以改用docker exec -it springboot-demo-app sh进入容器后手动装工具,或者直接跳过容器排查,在宿主机上先看数据库端口是否正常监听。很多时候,问题不在容器网络,而在数据库配置本身。

3.2 使用 --net=host 的临时方案和它的局限

网上有一种说法是给docker run加--net=host,容器共享宿主机的网络栈,这样容器里访问localhost就等于访问宿主机。命令确实能跑:

docker run -d -p 8080:8080 --name springboot-demo-app --net=host springboot-demo:1.0.0

但我不推荐把这当成正式方案。第一,在 Linux 上用 host 网络时,-p端口映射参数会被忽略,容器直接占用宿主机端口,多个容器之间容易冲突。第二,Windows Docker Desktop 对 host 网络的支持并不完整,经常出现“看起来加了参数,实际上没生效”的情况。第三,如果数据库部署在另一个服务器或另一个容器,host 网络并不能解决所有连通问题。

所以,我一般只把这个参数用在临时调试上,真正要稳定连接数据库,还得靠自定义网络 + 容器名解析。

3.3 创建自定义网络,让容器名互通

最稳妥的做法是创建一个自定义 bridge 网络,把 Spring Boot 容器、MySQL 容器、Redis 容器都放进去。在同一个网络里,容器之间可以直接用容器名互相访问,Docker 自带的 DNS 会解析到对应 IP。

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

docker network create dev_net创建了一个名为dev_net的网络。docker network connect可以把一个已经存在的容器加入该网络,支持在容器运行中操作。这里mysqldoc、redis、springboot-demo-app分别是容器名,你可以按自己的名称替换。

更推荐的做法是,docker run创建 Spring Boot 容器时直接指定网络,避免容器同时挂在两个网络下产生不确定的路由:

docker run -d -p 8080:8080 --network dev_net --name springboot-demo-app springboot-demo:1.0.0

这样容器启动时就属于dev_net,与 MySQL、Redis 之间用容器名访问,IP 随便变都不影响。

3.4 修改 yml 配置并重建容器

网络配好后,Spring Boot 项目里application.yml的数据源地址就不能写成 IP 了,要改成容器名。下面是一个典型配置:

spring: datasource: url: jdbc:mysql://mysqldoc:3306/yourdb?useSSL=false&serverTimezone=Asia/Shanghai username: root password: root redis: host: redis port: 6379 password: redis

注意,这里写的端口是容器内部端口,MySQL 是 3306,Redis 是 6379,不是宿主机对外映射的 3307、6380。容器间通信走内部端口,外部工具才走映射端口,这是最容易混的地方。如果你 MySQL 容器映射到了宿主机 3307,那只是给宿主机和外部工具用的,容器内部访问 MySQL 依然用 3306。

改完 yml 后,需要重新打包 jar,再重新构建镜像并重建容器。Spring Boot 的配置在 jar 内部,不重打镜像就不会生效,运行时在容器里改配置文件是没用的。完整流程就是:

mvn clean package -DskipTests docker build -t springboot-demo:1.0.0 . docker stop springboot-demo-app docker rm springboot-demo-app docker run -d -p 8080:8080 --network dev_net --name springboot-demo-app springboot-demo:1.0.0

docker stop是停止容器,docker rm是删除容器。因为容器名不能重复,旧容器没删干净,直接docker run --name会报 name already in use。

3.5 数据库授权:别让 host 字段卡住你

哪怕网络通了,MySQL 也可能拒绝连接,因为在 MySQL 里用户是绑定 host 的。我一般会进入 MySQL 容器创建一个允许任意来源的账号,免得被 IP 问题卡住。

docker exec -it mysqldoc mysql -uroot -p

执行下面的 SQL:

CREATE USER 'root'@'%' IDENTIFIED BY 'root'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;

'root'@'%'表示这个用户可以从任意 IP 连接,如果你只想让某个开发机连接,可以把%换成具体 IP,比如'root'@'192.168.0.27'。但如果你用容器名mysqldoc访问,实际来源 IP 是容器网络里的地址,限制太严反而连不上,所以开发环境我一般直接用%。

MySQL 8 默认用caching_sha2_password认证插件,如果你的 JDBC 驱动太老,可能报Authentication plugin 'caching_sha2_password' cannot be loaded。可以执行下面这条 SQL 兼容一下:

ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root'; FLUSH PRIVILEGES;

这个坑在旧项目里特别常见,服务端和客户端驱动版本不一致时出现频率很高。

4. 把 MySQL 和 Redis 也容器化:挂载目录与端口映射的取舍

4.1 先规划目录:数据卷必须落在宿主机上

Spring Boot 连接数据库的问题是解决了,但如果你还没有数据库容器,或者数据库不在目标 Linux 上,那这一步就得把 MySQL 和 Redis 也容器化。容器本身是无状态的,一删就什么都没了,所以数据必须通过-v挂载到宿主机目录。

在 Windows 上,我习惯把所有 Docker 数据卷集中放到一个目录,比如D:\development_sources\docker。先建四个 MySQL 相关目录:

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

这四个目录分别对应容器内的日志目录、数据文件目录、配置文件目录和文件导入导出目录。mysql-files对应 MySQL 的secure_file_priv权限,导入导出 CSV 时会用到。路径里的反斜杠在 PowerShell 里可以直接用,但如果你切到 Git Bash 或 WSL 里执行,路径格式要改成/d/development_sources/...,否则 Docker 不认。

4.2 启动 MySQL 8.0.24 容器

目录准备好后,执行 MySQL 容器创建命令。我这边宿主机 3306 已经被本机 MySQL 占用了,所以将容器 3306 映射到宿主机的 3307。

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

-p 3307:3306将容器内 MySQL 默认端口 3306 映射到宿主机 3307,外部访问用 3307,容器内部还是 3306。--restart=always指定容器在 Docker 重启或异常退出时自动启动,这个参数对服务器环境很重要。-v后面四组分别把日志、数据、配置、导入导出目录挂载到宿主机。-e MYSQL_ROOT_PASSWORD=root在容器第一次初始化时设置 root 密码,如果data目录里已经有旧数据,这个环境变量会被忽略。

这里有一个细节:如果data目录是第一次挂载,容器会自动初始化 MySQL 数据。如果这个目录之前已经有别的一套 MySQL 数据,再用同一个镜像去启动,可能会报版本或权限不匹配。所以迁移镜像时,数据目录要一并备份或迁移,不能只搬容器。

4.3 启动 Redis 容器

Redis 相对于 MySQL 要简单一些,只需要一个数据目录。我先建目录,再启动容器:

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 也被占用,所以把 Redis 容器端口映射到宿主机 6380。-v把宿主机目录挂载到容器内/data,Redis 的持久化文件会写在这里。最后的redis-server --appendonly yes --requirepass redis是覆盖容器默认启动命令,开启 AOF 持久化并设置访问密码为redis。

--appendonly yes表示每个写操作都会追加到 AOF 文件,重启后可以恢复数据,适合对数据安全性要求高的场景。--requirepass redis是设置密码,如果这里不设,Spring Boot 配置里的password: redis就会报认证失败。外部工具连接时,端口填 6380,密码填redis。

4.4 将 MySQL 和 Redis 连接到 dev_net,并做外部访问验证

容器都起来后,需要把它们加进dev_net,这样 Spring Boot 容器才能用容器名访问。如果之前已经创建过网络,用下面命令直接连接:

docker network create dev_net docker network connect dev_net mysqldoc docker network connect dev_net redis

docker network create如果遇到“network already exists”的报错,说明网络已经存在,直接跳到 connect 就行。连接完成后,外部工具有一张参数表可以参考:

服务容器名容器内端口宿主机映射端口密码
MySQLmysqldoc33063307root
Redisredis63796380redis

用 Navicat 连 MySQL 时,主机填宿主机 IP,端口填 3307,用户填 root,密码填 root。用 Another Redis Desktop Manager 连 Redis 时,地址填宿主机 IP,端口填 6380,密码填 redis。如果从其他电脑连不上,优先查宿主机防火墙是否放行了 3307 和 6380,以及云服务器的安全组规则。

5. Windows 到 Linux 的镜像搬运:docker save 导出、scp 上传、docker load 加载

5.1 导出镜像:用 docker save 而不是 docker export

Windows 上的环境都调试好后,要把整套东西搬到 Linux。这里有个关键点:要导出的是镜像,不是容器。docker export导出的是容器文件系统,不包含镜像层历史,Load 回来后不能直接docker run成原本的镜像。所以必须用docker save。

docker save -o springboot-demo.tar springboot-demo:1.0.0 docker save -o mysql.tar mysql:8.0.24 docker save -o redis.tar redis

-o指定导出的 tar 包文件名,后面跟镜像名。如果你有多个镜像需要一次导出,也可以在一条命令里写多个镜像名,但我习惯分开导出,这样上传到 Linux 后可以单独加载,排查问题也更方便。注意镜像名要写完整,带 tag 就写 tag,缺了容易在 load 后出现<none>。

导出完成后,可以在 Windows 上看到这几个 tar 文件。文件体积一般不小,Spring Boot 镜像如果基础镜像大,可能几百 MB。如果网络带宽有限,可以先压缩再传,在 PowerShell 里可以直接用 tar 命令压缩:

tar -czf springboot-demo.tar.gz springboot-demo.tar

但优先建议直接传输原始 tar 包,免得在 Windows/Linux 之间压缩格式不兼容导致解压乱掉。

5.2 准备 Linux 环境:安装 Docker 并启动

我这边 Linux 服务器是 CentOS 7。拿到一个全新环境,先不急着传镜像,先把 Docker 装好。

如果是开发环境,我习惯先关防火墙,因为后面要放行 8080、3307、6380 等多个端口,省得一个个配置。生产环境千万别照做。

systemctl stop firewalld systemctl disable firewalld

然后更新 yum 包。yum -y update会连系统和内核一起升级,耗时较长;如果只想更新软件包不升内核,用yum -y upgrade。开发环境我一般直接 update,生产环境会保持谨慎。

yum -y update

Docker 安装前,先把可能残留的旧版本卸载干净,否则版本冲突很烦:

yum remove docker docker-common docker-selinux docker-engine

然后安装依赖工具和阿里云 yum 源:

yum install -y yum-utils device-mapper-persistent-data lvm2 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo yum list docker-ce --showduplicates | sort -r

yum-utils提供yum-config-manager,后两个是 Docker 存储驱动依赖。阿里云源在国内下载速度比官方源快得多。yum list可以列出所有可用版本,方便挑一个指定版本安装。我这次按照原环境选的是18.03.1.ce,这是一个比较老的版本,但在这台 CentOS 7 上能稳定跑。如果你的内核太新,反而建议选 20.10 以上的版本,具体可以对照 Docker 官方兼容矩阵。

yum -y install docker-ce-18.03.1.ce systemctl start docker systemctl enable docker docker version

systemctl start docker启动 Docker 服务,systemctl enable docker设置开机自启。docker version能同时看到 client 和 server 版本,如果只显示 client 而 server 报错,说明 daemon 没起来,看/var/log/messages或运行systemctl status docker。

5.3 上传镜像并加载到 Linux

Docker 装好后,把 Windows 上的 tar 包传到 Linux。我用的是 Finalshell 这类带图形界面的工具,直接拖文件上传很方便。命令行场合下,scp 更快:

scp springboot-demo.tar mysql.tar redis.tar root@192.168.x.x:/root/loadimages/

上传后在 Linux 上加载镜像:

cd /root/loadimages docker load -i springboot-demo.tar docker load -i mysql.tar docker load -i redis.tar docker images

docker load -i是从指定 tar 文件导入镜像。导入后docker images应该能看到与 Windows 上一致的镜像名和 tag。如果看到<none>,说明导出时镜像仓库和标签信息丢了,需要用docker tag重新打标。

镜像加载完之后,运行容器时数据卷路径必须改为 Linux 路径。Windows 的D:\development_sources\docker\mysql\data在 Linux 上是不存在的,直接复用命令会导致挂载失败。我在 Linux 上习惯把数据放到/data/docker下:

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

然后启动 MySQL:

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

Redis 同理:

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

Spring Boot 容器启动前,记得先创建网络并把数据库容器加进去:

docker network create dev_net docker network connect dev_net mysqldoc docker network connect dev_net redis docker run -d -p 8080:8080 --network dev_net --name springboot-demo-app springboot-demo:1.0.0

注意:网络dev_net不会随镜像一起被 save/load,所以到 Linux 后必须重新创建。这一步漏了,容器名解析就会失败,应用照样连不上数据库。

6. 镜像迁移与容器联调中的常见问题避坑:现象、原因、解决

6.1 Windows 数据卷路径直接搬到 Linux 导致挂载失败

  • 现象:在 Linux 上执行原本 Windows 用的docker run命令,容器启动失败,docker logs mysqldoc里报mkdir: cannot create directory '/var/lib/mysql'或chown: /var/lib/mysql: operation not permitted。
  • 原因:Windows 路径D:\development_sources\docker\mysql\data在 Linux 上不存在,Docker 会尝试创建一个名为D:的相对路径或直接报错,导致挂载源无效。
  • 解决:把-v左侧的宿主机路径改成 Linux 绝对路径,比如/data/docker/mysql/data,并提前用mkdir -p建好目录。如果数据目录权限不对,可以执行chown -R 999:999 /data/docker/mysql/data,因为 MySQL 容器内部用户 UID 是 999。

6.2 容器重启后 IP 变化导致 yml 写了固定 IP 连不上

  • 现象:Spring Boot 容器第一次能连上数据库,重启容器后报Connection refused,检查容器 IP 发现已经变了。
  • 原因:默认 bridge 网络的容器 IP 是动态分配的,重启后可能变。application.yml里如果写的是容器 IP,IP 一变就失效。
  • 解决:使用自定义网络,并把 yml 里的数据库地址改成容器名。创建网络docker network create dev_net,Spring Boot 容器启动时加--network dev_net,MySQL 容器也连接该网络,然后 yml 里写mysqldoc:3306而不是 IP。这样无论容器 IP 怎么变,Docker 内部 DNS 都能解析到容器名。

6.3 docker save 导出镜像后 load 到 Linux 出现 标签

  • 现象:docker images里多了一个<none>的镜像名,用docker run springboot-demo:1.0.0报找不到镜像。
  • 原因:docker save时没有写完整 tag,或者原镜像本身就是<none>。比如docker save -o demo.tar 5f02c1e8a6a3只保存了镜像 ID,repository 信息为空,load 回来自然没有名字。
  • 解决:导出前先确认docker images里镜像名和 tag 完整。load 后发现<none>,用docker tag补上镜像名:
docker tag <镜像ID> springboot-demo:1.0.0

<镜像ID>用docker images里的 IMAGE ID 前几位即可。

6.4 防火墙关了,从其他电脑还是连不上容器端口

  • 现象:systemctl stop firewalld已经执行,在 Linux 本机curl localhost:8080能通,但从其他电脑连不上 8080 或 3307。
  • 原因:不是 firewalld 拦截,而是 SELinux 或云安全组在起作用。CentOS 7 的 SELinux 可能拦截 Docker 端口映射,云服务器厂商的安全组也独立于系统防火墙。
  • 解决:先在 Linux 上确认端口在监听,执行ss -tlnp | grep 3307,如果能看到监听列表,再执行getenforce查看 SELinux 状态,临时关闭可以用setenforce 0。如果问题出在公网入方向,需要到云控制台安全组放行对应端口。生产环境不建议全局关 SELinux,应该用semanage port -a -t http_port_t -p tcp 8080之类的方式放行。

6.5 宿主机端口被占用导致容器启动失败

  • 现象:执行docker run -p 3306:3306 mysql时报错bind: address already in use,容器根本没创建。
  • 原因:宿主机上已经有一个进程占用了 3306 端口,Docker 无法再把该端口映射给容器。
  • 解决:换一个宿主机端口,比如-p 3307:3306。容器内部端口不变,外部访问 3307 即可。这时候要注意,如果其他容器要访问 MySQL,应该用容器名mysqldoc和内部端口 3306,不要被映射端口带偏。

7. 装完怎么验证:从日志、端口、访问到自启动,一条命令都不少

整套环境在 Linux 上搭好后,我一般会强制自己按下面四步走一遍,每步都能揪出不同层面的问题。

第一步,确认容器都在运行。执行docker ps,看 mysqldoc、redis、springboot-demo-app 的STATUS是不是Up,PORTS列有没有显示0.0.0.0:8080->8080/tcp、0.0.0.0:3307->3306/tcp、0.0.0.0:6380->6379/tcp。如果有容器变Exited,马上docker logs 容器名,先处理启动报错。

第二步,验证应用接口。在 Linux 本地执行curl http://localhost:8080/actuator/health,或者你自己项目里定义的某个接口。能返回 JSON 说明 Spring Boot 起来了;如果返回连接错误,马上看日志,多半是数据库或 Redis 地址没改对。注意,有的 Spring Boot 接口没加 health 端点会返回 404,这是正常的,这时候直接看docker logs -f springboot-demo-app最后几行有没有Started就够。

第三步,验证外部工具连接。在另一台电脑上,用 Navicat 连宿主机IP:3307,用户 root,密码 root;用 Another Redis Desktop Manager 连宿主机IP:6380,密码 redis。连不上就一层层排查:先ss -tlnp | grep 3307看端口是否监听,再执行getenforce看 SELinux,最后检查云安全组。这套顺序能定位绝大多数“本地能连,远端连不上”的问题。

第四步,重启 Docker 验证自启。执行systemctl restart docker,等十几秒后再docker ps,--restart=always的那几个容器必须自动回到Up状态。如果 Spring Boot 容器没起来,很可能是它比 MySQL 先启动了,JDBC 连接池还没等到数据库就绪。这时候可以在 docker run 命令里加--restart=always并配合 Spring Boot 的重试机制,或者用 Docker Compose 的depends_on加condition: service_healthy控制启动顺序。

从那以后,我每次迁移 Spring Boot 镜像到 Linux,都会强制自己走一遍docker ps、docker logs、curl 接口、systemctl restart docker这四个动作,缺一个都可能把问题留到半夜。这次记录下来的数据卷路径、容器名解析、端口映射冲突这些坑,也都是一次次翻车后总结出来的。希望帮到你,照着这套流程走完,Windows 上构建的镜像到 Linux 上跑通,不会比你想象中更难。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进