ARTICLE DETAIL

资讯详情

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

Docker容器访问宿主机MySQL:四种连接方案与排查指南

Docker容器访问宿主机MySQL:四种连接方案与排查指南 最近群里有好几个朋友都在问同一个问题后端项目已经容器化跑起来了MySQL 却还装在宿主机上容器里的 Java 服务到底怎么连过去这个问题在 Docker、容器、宿主机、MYSQL 这四个关键词相关的话题下被反复提起尤其是刚把 Spring Boot 项目打成镜像、又在宿主机上装了 MySQL 8.0 的人几乎都会卡在这一步。容器内项目访问宿主机 MySQL看起来只是改个连接地址的事但里面牵扯到 Docker 网络模式、MySQL 授权规则、防火墙策略这几个环节任何一个没对齐都会连不上。这篇文章我会从方案选型、网络原理、具体配置、常见报错四个层面把“容器项目访问宿主机 MySQL”这件事完整拆开。无论你是用 docker run 还是 docker-compose无论宿主机是 Windows、macOS 还是 Linux都能在这里找到能直接抄的配置和排查路径。适合正在做本地开发联调、或者小规模单机部署的开发者参考也适合刚学 Docker 不久、对网络概念还比较模糊的新手对照着理解。1. 先把场景和方案讲清楚1.1 什么时候需要容器访问宿主机 MySQL先梳理一下最常见的几种使用场景。第一种是本地开发环境你在 Windows 或 macOS 上装了 Docker DesktopMySQL 直接装在宿主机上项目代码在容器里跑数据库想用宿主机这份现成的省得在容器里再维护一份数据。第二种是单机部署场景服务器上已经有一套 MySQL 实例里面有历史数据、定时备份、监控脚本你不想把数据库迁移进容器只想把新写的业务服务容器化让服务去连宿主机上那个 MySQL。第三种是混合架构一部分组件已经在容器里跑但数据库因为版本、运维习惯或者性能调优的原因继续留在宿主机上。这三种场景的诉求本质都一样容器里的应用需要访问宿主机上监听在 3306 端口的 MySQL 服务。但这个诉求有一个先决条件——Docker 容器的网络命名空间是独立的容器里的 localhost 指向的是容器自己不是宿主机。所以你不能在容器里写 jdbc:mysql://localhost:3306 来连宿主机 MySQL这条路径从一开始就是错的。要解决这个问题核心思路就一句话让容器里的流量能路由到宿主机的网络栈上。实现这个目标有几种不同路径但殊途同归。1.2 四条常见访问路径选哪条最省事我把实践中常用的方案归成四类你可以根据自己的场景对号入座宿主机 IP 直连在容器里用宿主机的局域网 IP 来连接。比如宿主机 IP 是 192.168.1.100连接地址写 jdbc:mysql://192.168.1.100:3306。这是最朴素、最容易理解的办法适合宿主机和容器网络互通的所有场景。Docker 网桥网关 IP容器默认在 bridge 网络下宿主机在 docker0 网桥上有一个网关地址通常是 172.17.0.1。容器里可以拿这个地址访问宿主机。这个方案不用额外配置但前提是 MySQL 监听在 0.0.0.0 上。host 网络模式容器启动时指定 network_mode: host让容器直接共享宿主机的网络命名空间。此时容器里的 localhost 就是宿主机的 localhost连接地址可以原封不动写成 jdbc:mysql://127.0.0.1:3306。host.docker.internalDocker DesktopWindows/macOS内置支持这个主机名会指向宿主机Linux 上新版 Docker Engine 20.10 也可以用它但通常需要加 extra_hosts: host.docker.internal:host-gateway 配置。四选一没有绝对的最好只有最适合当前环境的一种。开发机上求省事Docker Desktop 的用户直接无脑用 host.docker.internalLinux 服务器上用网桥网关 IP 或者 host 模式都很常见如果 MySQL 和容器本身都在同一台机器上host 模式是最干净直接的但代价是失去 Docker 端口隔离能力。我个人的偏好是开发环境用 host.docker.internal部署环境用 bridge 网络加网关 IP这样两种环境里代码连接地址都不用频繁改。2. 一条命令都通不了之前先搞懂容器网络2.1 容器里的 localhost 不等于宿主机很多新手第一次踩坑就是因为在容器里敲 mysql -h localhost -P 3306结果报连接拒绝然后开始怀疑 MySQL 没启动。其实 MySQL 好好的问题出在 localhost 的指向。Docker 容器默认使用 bridge 网络每个容器有自己的网络命名空间、自己的回环接口。你在容器里看 /etc/hosts会发现文件里除了容器的 hostname还有一个 127.0.0.1 的本地回环地址。当应用去连 127.0.0.1:3306实际上是去连容器自身的 3306 端口而容器的这个端口上根本没跑服务自然连接失败。这就好比你在自己家里翻同事抽屉当然翻不出来东西——你们根本不是同一个“命名空间”。明白了这个你就知道为什么网上所有教程都强调“不要用 localhost 访问宿主机服务”。localhost 在容器里是隔离的宿主机上的 MySQL 默认监听地址如果是 127.0.0.1那容器更不可能碰到它。前者是网络隔离的问题后者是 MySQL 监听地址的限制两层因素叠加不通才是常态。2.2 docker0 网桥与 172.17.0.1 网关的本质那为什么容器能直接访问 172.17.0.1 上的服务这里要理解一个关键角色docker0 网桥。在默认 bridge 网络模式下Docker 会在宿主机上创建一个虚拟网桥 docker0它就像一个内部交换机所有普通容器都接到这个交换机上。这个网桥有一个 IP 段默认是 172.17.0.0/16网桥本身占用 172.17.0.1。容器创建时Docker 会从网段里分配一个 IP比如 172.17.0.2然后通过一对 veth 虚拟网线把容器和 docker0 连起来。容器内的默认路由会指向 172.17.0.1也就是说容器访问外部网络时第一个“跳板”就是 docker0 网桥。由于宿主机就是 docker0 网桥的持有者所以 172.17.0.1 实际上就是宿主机的“容器侧”网络地址。容器访问 172.17.0.1:3306数据包会通过 veth 虚拟网线到达 docker0再由宿主机的网络栈把数据转发出去。如果宿主机上的 MySQL 监听在 0.0.0.0 或 172.17.0.1 上这个连接就能顺利建立。这个逻辑适用于所有基于 bridge 网络的容器无论你用的是 docker run 不带网络参数还是 docker-compose 里没有指定 network_mode默认都走这条路。这里有个隐含要求MySQL 不能只监听 127.0.0.1。MySQL 的 bind-address 配置决定了服务绑定在哪个网络接口上如果 bind-address127.0.0.1那服务只接受来自本机回环接口的连接docker0 网桥上来的包会被直接丢弃你在容器里用 172.17.0.1 连接一样超时。所以生产环境排查时先看 MySQL 的 bind-address 是个关键动作。2.3 host.docker.internal 到底是何方神圣Docker Desktop 使用一种特殊的虚拟化网络宿主机上跑了轻量级 VM容器实际上运行在 VM 里。这种情况下容器访问常见的 172.17.0.1 不一定好用因此 Docker Desktop 内置了一个动态解析的主机名 host.docker.internal会自动指向宿主机在虚拟机网络中的地址。对用户来说代码里写 host.docker.internal 或者 mysql://host.docker.internal:3306 就能连到宿主机 MySQL非常优雅。Linux 上的原生 Docker 没有这个自动映射。但 Docker Engine 20.10 之后提供了一种机制启动容器时添加 --add-hosthost.docker.internal:host-gatewayDocker 会自动把这个主机名解析到 docker0 网桥的网关 IP也就是宿主机在容器网络里的地址。效果等同于你在 /etc/hosts 里手动写一行 172.17.0.1 host.docker.internal。理解这层原理之后你就能明白为什么网上很多教程在 Windows/macOS 上有效换到 Linux 上却失效因为它们没做 host-gateway 映射。这也是我特别提醒的一点看到 host.docker.internal 别高兴太早先确认宿主机的 Docker 是不是原生 Linux 环境。3. 实操三种方式连接宿主机 MySQL3.1 方式Abridge 网络 宿主机网关 IP 连接这是最通用、最少依赖的方式特别适合 Linux 服务器上的部署场景。操作上分为三步。第一步确认容器默认网络和网关 IP。启动容器时如果没有特别指定网络默认就是 bridge 网络。你可以在宿主机上执行 ip addr show docker0 查看 docker0 网关 IP一般是 172.17.0.1。也可以在容器里执行 docker exec 你的容器名 ip route输出中 default via 后面的地址就是它。第二步确认 MySQL 监听地址。在宿主机上执行 ss -antlp | grep 3306观察监听 IP。如果显示 127.0.0.1:3306说明 MySQL 只监听本机回环需要修改 /etc/mysql/mysql.conf.d/mysqld.cnf 或 /etc/my.cnf 里的 bind-address把它改成 0.0.0.0然后重启 MySQL。第三步在项目配置里写连接地址。Spring Boot 的 application.yml 里这样配置spring: datasource: url: jdbc:mysql://172.17.0.1:3306/yourdb?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai username: youruser password: yourpassword如果项目不是 Java 技术栈PHP 或 Node.js 同理把数据库 host 字段写成 172.17.0.1 即可。启动容器后用 docker exec -it 容器名 bash 进去执行 mysql -h 172.17.0.1 -u youruser -p 验证连通性能连上就说明这条链路没问题。这里要补充一步很多人容易遗漏的 MySQL 授权宿主机上的 MySQL 用户如果只授权了 localhost容器连接时会报 Access denied for user。容器从 docker0 网桥访问宿主机 MySQL在 MySQL 看到的客户端地址通常是宿主机的 IP 或网关 IP所以必须显式授权。授权语句后面专门讲。3.2 方式Bnetwork_mode: host 直接本机连接host 网络模式是另一种很彻底的做法。容器启动时不创建独立的网络命名空间而是直接复用宿主机的网络栈。在这种模式下容器里的一切 IP 和端口都是宿主机的连 localhost 也一样所以连接地址天然就是 jdbc:mysql://127.0.0.1:3306代码几乎不用改。docker run 时加参数docker run -d --name myapp --network host -e SPRING_DATASOURCE_URLjdbc:mysql://127.0.0.1:3306/yourdb myapp-image如果使用 docker-compose服务配置里写services: app: image: myapp-image network_mode: hosthost 模式最大的优点是简单直接、性能损耗极小因为没有 NAT 和网桥转发。但缺点也很明显容器内的端口直接暴露在宿主机上多个容器不能监听同一个 3306 端口端口隔离优势完全丧失。这个模式更适合对网络延迟敏感、或需要容器内服务访问宿主机大量端口的场景如果你只是连接一个 MySQL我个人会觉得不太值得牺牲 Docker 的端口隔离能力。还有一个隐藏坑host 模式下MySQL 那边看过来的客户端地址就是 127.0.0.1所以你的 MySQL 连接用户只需要授权 localhost/127.0.0.1 就行。这一点反而比 bridge 模式简单。3.3 方式Chost.docker.internal 优雅版如果你是 Docker Desktop for Windows 或 Docker Desktop for Mac 用户最省事的连接地址就是用 host.docker.internal。这个主机名在 Docker Desktop 里默认可用直接写到项目配置里就能连宿主机 MySQL不需要查任何 IP。Linux 原生 Docker 想用这个主机名需要手动加映射。docker run 时docker run -d --name myapp --add-hosthost.docker.internal:host-gateway \ -e SPRING_DATASOURCE_URLjdbc:mysql://host.docker.internal:3306/yourdb \ myapp-imagedocker-compose 里这样写services: app: image: myapp-image extra_hosts: - host.docker.internal:host-gateway environment: SPRING_DATASOURCE_URL: jdbc:mysql://host.docker.internal:3306/yourdbhost-gateway 这个关键字是 Docker 20.10 引入的意思是“宿主机网关地址”Docker 会自动在容器的 /etc/hosts 里写入一行映射把它指向 docker0 网桥 IP。实际效果和方式 A 一致但好处是代码里不用写死 172.17.0.1 这种环境相关的 IP可移植性更好。我自己的建议是所有新建项目连接宿主机数据库一律用 host.docker.internal 这个写法以后不管是换电脑、切换开发环境还是部署到 CI 机器只要把 extra_hosts 配好代码几乎不用动。3.4 MySQL 侧必须做好的五件事选好连接方式只是第一步MySQL 这边如果没处理好照样连不上。我把最容易踩的五个点列出来你在动手配置前先核对一遍。第一bind-address。MySQL 如果绑定在 127.0.0.1那只有从宿主机本机进程发起的连接才能建立。容器访问必须让 MySQL 监听在 0.0.0.0 或容器网络的网关 IP 上。改完配置记得重启服务否则不生效。第二创建远程访问专用用户。不要偷懒用 root 直接连更不要给 root 开放任意 IP 访问否则 MySQL 用户表被爆破的风险会显著增加。最小权限原则在数据库上永远是底线。CREATE USER appuser% IDENTIFIED BY StrongPassw0rd!; GRANT SELECT, INSERT, UPDATE, DELETE ON yourdb.* TO appuser%; FLUSH PRIVILEGES;这里的 % 表示允许从任意 IP 连接在实际场景里可以根据需要缩窄成 172.17.0.% 或具体 IP。这个授权操作要特别注意 MySQL 8.0 的默认认证插件问题下面第四点会细说。第三防火墙放行 3306。MySQL 服务改了 bind-address 后宿主机防火墙可能会拦住容器来源的连接。在 Ubuntu 上如果开了 ufw需要执行 ufw allow from 172.17.0.0/16 to any port 3306 或临时关闭 ufw 测试。CentOS 上检查 firewalld有需要就放行 docker 网络段的 3306 端口。第四MySQL 8.0 默认认证插件 caching_sha2_password。如果你的项目用老版本驱动比如 mysql-connector-java 5.1.x会导致连接时报 Authentication plugin caching_sha2_password cannot be loaded。解决方案有三条升级驱动到 8.0.x或者在 URL 加 allowPublicKeyRetrievaltrueuseSSLfalse或者在创建用户时指定老认证插件CREATE USER appuser% IDENTIFIED WITH mysql_native_password BY StrongPassw0rd!;第五端口冲突检查。如果宿主机 3306 端口已经被别的进程占用MySQL 可能起不来或监听异常。用 ss -antlp | grep 3306 确认监听正常Java 项目还可以用 nc -vz 172.17.0.1 3306 测试端口可达性。4. 高频报错和排查实录4.1 Cant connect to MySQL server (10061 / 111)这是最常见的报错Docker 容器里跑 Java 应用时常看到。Windows 上的报错是 Connection refused (10061)Linux 上是 Connection refused (111)本质都是 TCP 连接被拒绝。排查顺序很重要先确认宿主机 MySQL 真的在监听。在宿主机执行 ss -antlp | grep 3306。如果根本没输出说明 MySQL 没启动或者端口不是 3306。然后确认监听地址如果显示 127.0.0.1:3306MySQL 绑定的是回环接口容器根本访问不到。再确认容器到宿主机网络是否可达。在容器里执行 ping 172.17.0.1 或 telnet 172.17.0.1 3306。ping 不通说明网络链路有问题可能是自定义 bridge 网络导致网关 IP 变了telnet 不通而 ping 同大概率是防火墙拦截了端口。记住容器访问宿主机和宿主机访问容器走的是两条不同的网络路径不能拿宿主机本地能连 MySQL 来推断容器里也能连。排查完这层再用 MySQL 客户端验证一下docker exec -it myapp mysql -h 172.17.0.1 -u appuser -p如果这里能连上说明网络和授权都没问题再回头审视项目配置的连接串和驱动版本。4.2 Access denied for user 和 caching_sha2_passwordAccess denied 这个报错通常不是网络问题而是 MySQL 用户授权问题。错误信息里一般会指出是从哪个 IP 过来的连接被拒绝比如 Access denied for user appuser172.17.0.2。这种情况下检查两件事用户是否存在以及授权范围是否包含来源 IP。用 % 匹配所有 IP 是最省事的如果为了安全缩窄范围注意容器从 bridge 网络访问宿主机MySQL 看到的是 docker0 网段的 IP授权范围至少需要包含网段内的地址否则会被拒绝。MySQL 有两个独立的概念一个是客户端 TCP 连接本身成不成立另一个是 MySQL 层面用户身份认证通不通过很多人被 Access denied 卡住其实已经完成了 TCP 连接只是 MySQL 不认这个用户。caching_sha2_password 则是一个更隐蔽的坑。MySQL 8.0 开始新建用户的默认认证插件改成了 caching_sha2_password。这货的安全性是提高了但很多老驱动压根不认识这个插件。报错样式是 Public Key Retrieval is not allowed 或插件无法加载。前者可以在 JDBC URL 末尾加 allowPublicKeyRetrievaltrue 解决后者就要认命升级驱动或改用户认证方式。实测下来最快的处理办法是把项目里的 mysql-connector-java 升到 8.0 系列大部分问题迎刃而解实在没法升级的再去改用户认证插件。4.3 其他坑防火墙、IPv6、容器重启 IP 漂移防火墙是最容易被忽略的一环因为它只在你换网络环境时才冒出来。比如本地开发时容器访问宿主机 3306 是通的部署到云服务器上突然就不通了很多人的第一反应是 MySQL 配置变了其实是云服务器的安全组没放行 3306 入方向流量。还有 Linux 上开了 ufw 或 firewalld 的机器默认 INPUT 链策略如果是 DROP即使容器通过 docker0 的流量也会被拦需要在防火墙里放行 172.17.0.0/16 网段对 3306 的访问。IPv6 的问题比较小众但遇到就很迷惑。某些发行版上 Docker 默认启用 IPv6容器网络是 IPv6 地址而 MySQL 只监听了 IPv4 地址导致连接超时。检查方法是看 ss -tlnp 里 MySQL 监听的是 tcp 还是 tcp6以及容器里的 getent hosts 解析结果。多数情况下让 MySQL 也监听 IPv6 或者关闭 Docker 的 IPv6 支持都能解决。容器重启 IP 漂移的问题在 bridge 网络下特别值得注意。容器每次重建IP 地址可能从 172.17.0.2 变成 172.17.0.5如果 MySQL 授权时只精确匹配了原来那个 IP重建容器后连接又会失败。所以要么授权时用 % 或网段范围要么给容器绑定固定 IP要么用自定义网络并在 compose 里设置 ipam 固定地址。我倾向于第一种省事且安全可控。做一个快速对照表现象可能原因优先排查动作容器连 MySQL 报 Connection refusedMySQL 绑定 127.0.0.1 或没启动ss -antlp | grep 3306改 bind-address容器连 MySQL 超时防火墙阻断 docker0 到 3306ufw/firewalld 放行 docker 网段Access deniedMySQL 用户未授权容器来源 IPmysql -e select user,host from mysql.usercaching_sha2_password 报错驱动版本太老升级 JDBC 驱动或改 mysql_native_password容器重建后连不上IP 漂移导致授权失效授权用 % 或固定容器 IP5. 用 docker-compose 固化配置5.1 一段可直接抄的 compose 配置如果项目里有 docker-compose推荐把连接配置写成带 extra_hosts 的形式方便团队协作。services: app: image: myapp-image:latest container_name: myapp restart: unless-stopped extra_hosts: - host.docker.internal:host-gateway environment: SPRING_DATASOURCE_URL: jdbc:mysql://host.docker.internal:3306/yourdb?useSSLfalseallowPublicKeyRetrievaltrue SPRING_DATASOURCE_USERNAME: appuser SPRING_DATASOURCE_PASSWORD: StrongPassw0rd! ports: - 8080:8080这段配置在 Linux、macOS、Windows 上都能跑。extra_hosts 里的 host-gateway 会自动把 host.docker.internal 指向宿主机网关MySQL 侧必须完成上一节讲的 bind-address 修改和用户授权。如果你更习惯用 docker run 方式启动上面这段配置等效的命令是docker run -d --name myapp --add-hosthost.docker.internal:host-gateway \ -e SPRING_DATASOURCE_URLjdbc:mysql://host.docker.internal:3306/yourdb?useSSLfalseallowPublicKeyRetrievaltrue \ -e SPRING_DATASOURCE_USERNAMEappuser \ -e SPRING_DATASOURCE_PASSWORDStrongPassw0rd! \ -p 8080:8080 myapp-image:latest两种方式选一个就行我实际项目里更倾向 docker-compose因为 environment 的改动有记录、可评审不会出现“上次是哪条命令跑的谁也不知道”的情况。5.2 生产环境我还要再多说几句本地开发能通不代表生产环境能直接照搬。生产环境如果也是单机 Docker 加宿主机 MySQL方案基本一样但有几个地方要额外收紧。第一MySQL 授权用户不要用 %而是限制在 Docker 网段内比如 appuser172.17.0.%能显著减少暴露面。第二3306 端口最好不要在宿主机上对外开放只让 docker0 网桥或内网网卡可访问即可方法就是把 bind-address 设置成 172.17.0.1 或内网 IP而不是 0.0.0.0。第三连接串里务必启用 SSL 或至少设置 useSSLfalse 时确认网络环境可信生产库里明文传输还是有点心虚的。如果数据库数据量变大、要搞主从复制或者做读写分离宿主机单机 MySQL 的容量和可用性都会成为瓶颈那时候考虑把 MySQL 也容器化配合 Docker 网络里的服务名互访或者直接用云数据库反而架构更简单。容器化不是目的稳定好用才是目的这个度需要自己把握。另外再分享一个我常用的验证小技巧连接串改完之后先在容器里用命令行客户端测一次连通性通了再启动项目。这样出了问题你能快速定位是 MySQL 侧的问题还是应用侧的问题。很多人上来就重启应用日志刷一堆连接失败其实一条命令就能判断的事不需要反复试错。在使用 Docker 和宿主机 MySQL 的这两年我的体会是先从网络层面把“容器里看到的 IP 和我以为的 IP”这件事理顺剩下的一切都好解决。连接地址写哪个、MySQL 授权怎么写、防火墙放行哪段这些都是同一套逻辑衍生出来的细节。如果你正卡在某个报错上按排查表一层层过比盲目改配置要快得多。这篇内容里的所有配置和命令都是我在本地和服务器上实际验证过的你可以直接拿去用遇到问题再回来对着排查表找答案。
返回列表