背景:本地测试,项目启动依赖 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.yml 中 password: ${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-server 占 127.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/(初始化时自动执行)。
六、总结
- Mac 上 podman 服务连不上,先
lsof查端口归属------本机 Homebrew 服务(MySQL/Redis)开机自启抢端口是最隐蔽的坑 - 容器 MySQL 拒绝远程连接是账号 host 匹配问题 ,加
root@'%'即解 - Dockerfile 环境变量只在首次初始化生效,之后的密码改动都在数据卷里
- 容器间互访走内部网络(无 NAT、用容器名),Mac 程序访问容器走 gvproxy(有 SNAT)
- MySQL 账号匹配把
127.0.0.1/::1视作localhost,但经网关转发的连接源 IP 已改写,匹配不上