Mac + 容器本地 MySQL/Redis 环境搭建与网络原理实战

背景:本地测试,项目启动依赖 MySQL 和 Redis。本文记录用 Podman 在 Mac 上搭建这套环境的完整过程,以及踩坑后摸清的 Podman 网络原理。

一、容器搭建

1.1 Dockerfile(MySQL)

dockerfile 复制代码
FROM mysql/mysql-server:5.7
VOLUME ["/var/lib/mysql"]
EXPOSE 3306
CMD ["mysqld"]

注意:mysql/mysql-server 镜像首次初始化时强制要求 MYSQL_ROOT_PASSWORD,不给会启动失败。上面去掉密码的写法只在沿用已有数据卷 时可用;若删卷重建,需临时补回 ENV MYSQL_ROOT_PASSWORD=xxx

1.2 Dockerfile(Redis)

dockerfile 复制代码
FROM redis:7
VOLUME ["/data"]
EXPOSE 6379
CMD ["redis-server"]

Redis 默认无密码,与 application.ymlpassword: ${REDIS_PASSWORD:} 默认空串直接匹配。

1.3 构建与启动

bash 复制代码
cd /Users/zhouli/zl/docker-file/mysql
podman build -t my-mysql:5.7 .
podman run -d --name mysql57 -p 3306:3306 -v mysql57-data:/var/lib/mysql my-mysql:5.7

cd /Users/zhouli/zl/docker-file/redis
podman build -t my-redis:7 .
podman run -d --name redis7 -p 6379:6379 -v redis7-data:/data my-redis:7

# 验证
podman exec -it redis7 redis-cli ping          # PONG
podman logs -f mysql57                          # 等 "ready for connections"

1.4 一键启动脚本

脚本 start_mysql_redis.sh:先检查 podman machine 是否运行(没有则 podman machine start),再检查 mysql57/redis7 容器是否运行(已存在则 podman start,不存在则 build + run):

bash 复制代码
#!/bin/bash
set -e

MYSQL_DIR="/Users/zhouli/zl/docker-file/mysql"
REDIS_DIR="/Users/zhouli/zl/docker-file/redis"
MYSQL_NAME="mysql57"
REDIS_NAME="redis7"

# 1. 检查 podman machine
echo "==> 检查 podman machine ..."
if ! podman machine list 2>/dev/null | grep -q "Currently running"; then
    echo "==> podman machine 未运行,正在启动 ..."
    podman machine start
else
    echo "==> podman machine 已运行"
fi

# 2. 检查并启动 mysql
echo "==> 检查 ${MYSQL_NAME} ..."
if podman ps --format "{{.Names}}" | grep -q "^${MYSQL_NAME}$"; then
    echo "==> ${MYSQL_NAME} 已在运行"
else
    if podman ps -a --format "{{.Names}}" | grep -q "^${MYSQL_NAME}$"; then
        echo "==> 启动已存在的容器 ${MYSQL_NAME} ..."
        podman start "${MYSQL_NAME}"
    else
        echo "==> 构建 + 创建容器 ${MYSQL_NAME} ..."
        podman build -t my-mysql:5.7 "${MYSQL_DIR}"
        podman run -d --name "${MYSQL_NAME}" -p 3306:3306 -v "${MYSQL_NAME}-data:/var/lib/mysql" my-mysql:5.7
    fi
fi

# 3. 检查并启动 redis
echo "==> 检查 ${REDIS_NAME} ..."
if podman ps --format "{{.Names}}" | grep -q "^${REDIS_NAME}$"; then
    echo "==> ${REDIS_NAME} 已在运行"
else
    if podman ps -a --format "{{.Names}}" | grep -q "^${REDIS_NAME}$"; then
        echo "==> 启动已存在的容器 ${REDIS_NAME} ..."
        podman start "${REDIS_NAME}"
    else
        echo "==> 构建 + 创建容器 ${REDIS_NAME} ..."
        podman build -t my-redis:7 "${REDIS_DIR}"
        podman run -d --name "${REDIS_NAME}" -p 6379:6379 -v "${REDIS_NAME}-data:/data" my-redis:7
    fi
fi

echo "==> 完成。当前运行容器:"
podman ps

二、踩坑记录(按时间线)

坑 1:Public Key Retrieval is not allowed

复制代码
java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed

表面原因 :MySQL 8 的 caching_sha2_password 认证在 useSSL=false 时需要向服务器取 RSA 公钥,JDBC url 未允许。

解法(二选一):

properties 复制代码
# 方案 A:url 加参数
jdbc:mysql://...?useSSL=false&allowPublicKeyRetrieval=true&...

# 方案 B:用户改用 mysql_native_password + 空密码(不触发公钥交换)
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '';

注意:此坑后来被证明根因是"连错了 MySQL"------见坑 3。

坑 2:Host '192.168.127.1' is not allowed to connect

复制代码
Host '192.168.127.1' is not allowed to connect to this MySQL server

原因 :容器内 MySQL 只有 root@localhost 账号。MySQL 账号是 "用户@来源地址" 成对匹配的。Java 程序经 podman 网关转发进容器时,包的源 IP 被 SNAT 改写成网关 IP(192.168.127.1),MySQL 视其为"外地访客",root@localhost 匹配不上 → 拒绝。

解法:创建不限来源的 root:

sql 复制代码
CREATE USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

关键理解

  • MySQL 把 127.0.0.1::1 都视作 localhost,这三个来源走同一扇门
  • 但经 podman 网关转发的连接,源 IP 已被改写,永远匹配不上 localhost
  • 密码是"数据"(存在数据卷的 mysql 系统库),不是 Dockerfile 的"程序配置"。Dockerfile 的 MYSQL_ROOT_PASSWORD 只在首次初始化(数据目录为空)时生效一次

坑 3:连的根本不是容器的 MySQL(真·根因)

排查命令

bash 复制代码
lsof -nP -iTCP:3306 -sTCP:LISTEN
复制代码
COMMAND  PID   USER
mysqld   954  zhouli   TCP 127.0.0.1:3306 (LISTEN)   ← Mac 本机 brew 装的 MySQL 8!
gvproxy  7744 zhouli   TCP *:3306 (LISTEN)            ← podman 网关(容器侧)

问题 :Mac 本机 Homebrew 装的 mysql@8.0 开机自启,一直占着 127.0.0.1:3306。Java 连 localhost 优先解析到 127.0.0.1,命中的是本机 MySQL 8(不是容器里的 5.7)。坑 1 的 caching_sha2_password 报错、坑 2 之前的 Access denied (using password: NO) 全是它的锅。

解法

bash 复制代码
brew services stop mysql@8.0
brew services disable mysql@8.0   # 取消开机自启,状态变 none

同理 Redis:本机 redis-server127.0.0.1:6379,会抢在 gvproxy 之前被命中。Mac 上跑 podman 容器服务前,务必检查本机有无同端口服务

bash 复制代码
lsof -nP -iTCP -sTCP:LISTEN | grep -E "3306|6379"

坑 4:容器间互相访问的理解

两个容器(如 HTTP 服务 a、b)在同一 podman 网络 里,可以直接用容器内部 IP 互访(如 curl http://10.88.0.4:port),包从 a 的网卡出 → 桥接 → 直接进 b,全程不出虚拟机、不经过 gvproxy、没有 SNAT------b 看到的来源就是 a 的真实 IP。

与"Mac 程序 → 容器"的区别:

场景 源 IP 走 gvproxy?
Mac 程序 → 容器 被改成网关 IP
容器 a → 容器 b(同网络) 保持 a 自己的 IP

实践建议 :容器间调用用容器名而非 IP(同网络内置 DNS,IP 重启会变)。

三、Podman 网络原理(Mac 特有架构)

3.1 整体链路

复制代码
[Mac 上的 Java 程序]
   │ ① 连 localhost:3306
   ▼
[Mac 内核] 发现 *:3306 由 gvproxy 监听
   │ ② TCP 连接交给 gvproxy
   ▼
[gvproxy] 查端口映射表(podman run -p 时的登记)
   │ ③ 封新包,源 IP 改写为网关 IP(SNAT)
   ▼
[虚拟机内私有网络]
   │ ④ 送到目标容器 IP
   ▼
[容器] MySQL 收包,看到的来源是网关 IP

3.2 gvproxy 是什么

  • podman machine 自带的网关进程,跑在 Mac 宿主机上
  • 所有 -p 映射的宿主端口都由它监听(*:3306*:6379,即所有网卡)
  • 它一头连 Mac 本机网络,一头通过虚拟网络连虚拟机,负责双向转发
  • 同一进程在两侧各有一个 IP :Mac 侧是 *(本机监听),虚拟机私网侧是 192.168.127.1------像一个人在两个办公室各有一部电话

3.3 IP 查询常用命令

bash 复制代码
# 查容器内部 IP(实测 mysql57=10.88.0.4,redis7=10.88.0.5)
podman inspect -f '{{.Name}}  IP: {{.NetworkSettings.Networks.podman.IPAddress}}' mysql57

# 扫全部容器
podman ps -q | xargs podman inspect -f '{{.Name}}  {{.NetworkSettings.Networks.podman.IPAddress}}'

# 查映射端口归属
podman port mysql57        # 3306/tcp -> 0.0.0.0:3306

# 查 Mac 上谁在监听端口
lsof -nP -iTCP:3306 -sTCP:LISTEN

实测网段是 10.88.0.0/16(容器互访网段),而 MySQL 拒绝报错里的 192.168.127.1 是 podman 给"Mac 进来的流量"另划的网段。两者用途不同。

3.4 端口占用规则

  • 容器内部端口:不同容器可以重复(网络空间隔离,互不知晓)
  • 宿主机映射端口 :全局唯一,-p 3306:... 被占后再映射同端口报 address already in use
  • 绕法:错开宿主侧端口 -p 3307:3306,Mac 上连 localhost:3307 等于容器里的 3306

3.5 Podman 与 Docker 的差异

  • 无常驻守护进程-d 容器由 conmon 托管,机器重启后不会自动拉起
  • Mac 上 podman 前置条件:podman machine start(Linux VM 必须运行)
  • podman machine stop 会停整个 VM,容器全部停止;重启 VM 后需 podman start <容器名>

四、应用侧配置

4.1 配置方式(IDEA 启动)

application.yml 中的占位符 ${DB_PASSWORD:} 表示启动时读环境变量,没设取默认空串。

IDEA 设置:Run → Edit Configurations → Environment variables → 填 DB_PASSWORD=xxx(多个用 ; 分隔)。

配置优先级:命令行参数 > 环境变量 > system properties(-D)> yml。

4.2 最终可用配置

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
    username: ${DB_USERNAME:root}
    password: ${DB_PASSWORD:}    # 容器 MySQL 密码已置空,无需配置
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}   # Redis 无密码

4.3 建表

bash 复制代码
podman exec -i mysql57 mysql -uroot test < scripts/sql/test.sql
podman exec -i mysql57 mysql -uroot test < scripts/sql/test2.sql

# 验证
podman exec -it mysql57 mysql -uroot test -e "SHOW TABLES;"

五、数据持久化与密码的关系

操作 用户/密码改动是否保留
容器重启(podman restart ✅ 保留
删容器重建,沿用同卷 ✅ 保留
删容器重建且删卷 ❌ 回到 Dockerfile 初始状态

想让"出厂自带"某配置,可把 SQL 放进镜像的 /docker-entrypoint-initdb.d/(初始化时自动执行)。

六、总结

  1. Mac 上 podman 服务连不上,先 lsof 查端口归属------本机 Homebrew 服务(MySQL/Redis)开机自启抢端口是最隐蔽的坑
  2. 容器 MySQL 拒绝远程连接是账号 host 匹配问题 ,加 root@'%' 即解
  3. Dockerfile 环境变量只在首次初始化生效,之后的密码改动都在数据卷里
  4. 容器间互访走内部网络(无 NAT、用容器名),Mac 程序访问容器走 gvproxy(有 SNAT)
  5. MySQL 账号匹配把 127.0.0.1/::1 视作 localhost,但经网关转发的连接源 IP 已改写,匹配不上
相关推荐
吠品1 小时前
Nginx负载均衡配置与线上故障排查的一些经验
java·开发语言·数据库
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day44|Vue 前端项目搭建 思维导图复盘
java·开发语言·spring boot·学习·阿里云
万年咸鱼1 小时前
public class 和 class 的区别详解
java
焦虑的说说1 小时前
redis知识汇总
java·redis
Wang's Blog1 小时前
Java 接入Redis: 密码认证与远程连接配置
java·服务器·redis
SunnyDays10112 小时前
Java 拆分 Word 文档教程:按页、分页符和分节符拆分
java·word·分页·拆分
Dovis(誓平步青云)2 小时前
突破 32 位瓶颈:64 位 XID 如何化解事务号回卷危机
运维·服务器·人工智能·docker·容器
彧azz2 小时前
Java学习记录:判断语句
java·笔记·学习·算法