Redis 这个组件,但凡做过后端或者搞过本地开发环境的人都不陌生。它轻、快、上手简单,但真正要在自己的 Mac 上把它跑起来,很多人第一反应还是brew install redis然后redis-server一把梭。这么做当然能用,可一旦你手上同时有好几个项目、每个项目依赖的 Redis 版本不一样,或者你想模拟一套接近线上的多实例环境,本地直接装的方式就开始变得难受了——版本冲突、配置文件互相覆盖、卸载残留,这些问题会一个接一个冒出来。所以这几年我基本不再往 Mac 本机直接装 Redis 了,而是统一走 Docker,把数据库当成一个可以随时创建、随时销毁的容器来管理。这篇内容就是把我自己在 MacOS 上用 Docker 部署 Redis 的完整流程、踩过的坑和调优经验摊开讲清楚,从环境准备到持久化配置,再到常见故障排查,适合刚接触容器化开发的新手,也适合想把手头环境整理得更干净的老手参考。
1. 为什么在 Mac 上我更倾向于用容器跑 Redis
1.1 本机直装 Redis 的几个隐性成本
很多人觉得 Redis 就是个单文件的可执行程序,装在本机最省事。这话在只维护一个项目的阶段没错,但项目一多,问题就来了。brew install redis默认装的是最新稳定版,装完之后配置文件落在/usr/local/etc/redis.conf(Intel 芯片)或者/opt/homebrew/etc/redis.conf(Apple Silicon),服务通过brew services start redis常驻后台。表面上看很优雅,实际上你已经被绑定在一个固定版本上了。
我遇到过最典型的情况是:一个老项目依赖 Redis 5 的某些行为特性,而新项目想用 Redis 7 的新命令,本机只能装一个版本,来回切换得靠卸载重装,配置还得手动备份恢复。另一个隐性成本是端口占用,本机装的 Redis 默认吃 6379,你想再起一个测试实例就得改端口、改配置、改 pid 文件路径,改到后面自己都记不清哪个配置文件对应哪个实例。再加上卸载时残留的日志、dump 文件、plist 服务项,清理起来相当琐碎。
容器化恰好把这些成本一次性抹平:每个 Redis 实例是一个独立容器,版本、端口、配置、数据目录全部隔离,用完docker rm一删,干干净净,不留任何尾巴。
1.2 Docker Desktop 在 Mac 上的运行模型
这里得先讲清楚一个容易被忽略的原理,否则后面排查问题会一头雾水。MacOS 上没有原生的 Linux 内核,Docker 在 Mac 上并不是直接跑在宿主机上的,而是跑在一个轻量级虚拟机里(Docker Desktop 早期用 HyperKit,现在新版用自家的虚拟化框架)。也就是说,你的 Redis 容器实际上运行在那台虚拟机内部,而不是直接跑在 macOS 上。
这个模型带来两个直接影响。第一,容器的网络和 Mac 宿主机的网络之间隔了一层,端口映射是必须的,你不能指望容器里的 6379 直接等于 Mac 的 6379。第二,数据卷的读写要经过虚拟机的文件系统共享层,性能会比原生 Linux 上跑 Docker 略低,尤其是大量小文件读写时差异明显。理解这一点,后面配置持久化目录和排查连接问题时就不会走弯路。
1.3 版本选择:latest 还是锁定小版本
拉镜像时docker pull redis默认拿的是latest标签,我不建议在生产思维的项目里用 latest。原因很简单,latest 会随着官方更新而漂移,今天拉的是 7.2,下个月重建容器可能就变成 7.4 了,行为差异虽然小,但在调试阶段足够让你怀疑人生。
我的习惯是锁定到具体的小版本,比如redis:7.2-alpine。选 alpine 版本是因为它基于 Alpine Linux,镜像体积只有几十 MB,启动快、占用小,功能上和完整版没有区别,日常开发和中小规模使用完全够。如果你需要一些依赖 glibc 的扩展模块,才考虑用redis:7.2这种基于 Debian 的完整镜像。版本锁定这件事,本质上是把"环境不确定性"这个变量从你的调试方程里剔除掉,代价只是偶尔手动升级一次镜像标签。
2. 环境准备与镜像拉取的实际操作
2.1 确认 Docker 环境是否就绪
动手之前先确认 Docker 装好了并且守护进程在跑。打开终端执行:
docker version正常情况会输出客户端和服务端两段信息。如果只看到 Client 部分、Server 部分报错说连不上守护进程,说明 Docker Desktop 没启动,去应用程序里点开它,等托盘图标变成稳定状态再试。另一个常用检查命令是:
docker info这个命令会输出更详细的信息,包括容器数量、镜像数量、存储驱动、可用的 CPU 和内存资源。我特别建议看一眼输出里的Total Memory,因为 Docker Desktop 默认分配给虚拟机的内存是有限的(早期版本默认 2GB,新版会动态调整),如果你后面要跑多个容器,内存不够会导致容器被 OOM kill,排查起来很费劲。
2.2 拉取镜像与验证
确认环境没问题后拉取镜像:
docker pull redis:7.2-alpine拉完之后用docker images确认一下,能看到redis仓库、7.2-alpine标签、对应的 IMAGE ID 和体积。这里有个小技巧:如果你不确定某个标签存不存在,可以先docker search redis看看官方仓库的情况,或者直接去镜像仓库页面查标签列表,避免拉一个不存在的标签报错。
提示:拉镜像时如果卡在某一层很久,多半是网络到镜像仓库的链路问题,可以稍等重试,或者检查 Docker Desktop 的镜像加速配置。这一步和具体业务无关,纯粹是网络环境问题,耐心处理即可。
2.3 目录规划:把配置和数据放在哪里
容器本身是无状态的,但 Redis 的数据和配置需要落在宿主机上才能持久。我习惯在用户目录下建一个统一的工作区,比如:
mkdir -p ~/docker/redis/conf mkdir -p ~/docker/redis/dataconf放配置文件,data放持久化的 RDB 和 AOF 文件。为什么不用容器内部的默认路径?因为容器一删,内部数据全没了。把这两个目录挂载到宿主机,容器重建时数据还在,配置也能复用。这个目录结构看起来简单,但它是后面所有持久化配置的基础,值得一开始就规划清楚。
3. 启动容器的两种姿势与参数拆解
3.1 一条命令快速起一个开发用实例
如果你只是想快速验证、跑个 demo,最省事的方式是直接docker run:
docker run -d \ --name redis-dev \ -p 6379:6379 \ redis:7.2-alpine逐段拆解这条命令。-d表示后台运行,容器启动后终端立刻返回。--name redis-dev给容器起个名字,方便后续docker stop redis-dev、docker logs redis-dev直接引用,不然你就得记那一长串随机 ID。-p 6379:6379是端口映射,冒号左边是 Mac 宿主机端口,右边是容器内端口,Redis 默认监听 6379,所以两边都写 6379。最后是镜像名加标签。
这条命令起出来的实例没有密码、没有持久化挂载、用的是默认配置,只适合临时测试。跑起来之后可以用docker ps确认状态是 Up,然后本地用redis-cli连一下验证:
redis-cli -h 127.0.0.1 -p 6379 ping返回PONG就说明通了。注意这里-h写127.0.0.1而不是localhost,某些环境下 localhost 解析到 IPv6 的::1会导致连接失败,写死 IPv4 更稳。
3.2 带配置文件和持久化的正式启动方式
真正日常使用,我会用挂载配置和数据目录的方式启动:
docker run -d \ --name redis-prod \ -p 6379:6379 \ -v ~/docker/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf \ -v ~/docker/redis/data:/data \ redis:7.2-alpine \ redis-server /usr/local/etc/redis/redis.conf这里多了两个-v挂载。第一个把宿主机的配置文件挂到容器内的/usr/local/etc/redis/redis.conf,第二个把数据目录挂到/data,这是 Redis 官方镜像约定的数据存放路径。最后一行redis-server /usr/local/etc/redis/redis.conf是覆盖容器默认启动命令,让 Redis 用我们指定的配置文件启动,而不是用镜像内置的默认配置。
注意:挂载配置文件时,宿主机上的
redis.conf必须提前存在,否则 Docker 会把它当成一个目录来创建,导致启动失败。这是个非常经典的坑,我第一次用的时候就在这里卡了半天,报错信息还不直观。
3.3 用 docker-compose 管理更省心
命令行的方式适合一次性操作,但参数一多就容易记错。我后来统一改成用 compose 文件管理,把启动参数固化下来:
services: redis: image: redis:7.2-alpine container_name: redis-prod ports: - "6379:6379" volumes: - ./conf/redis.conf:/usr/local/etc/redis/redis.conf - ./data:/data command: redis-server /usr/local/etc/redis/redis.conf restart: unless-stopped把这份docker-compose.yml放在~/docker/redis/目录下,然后在该目录执行docker compose up -d就能起来。restart: unless-stopped这个策略值得说一下,它表示除非你手动 stop,否则容器异常退出或 Docker 重启后会自动拉起,对开发环境来说很实用,省得每次开机都要手动启动。用 compose 的另一个好处是配置即文档,团队里谁拿到这份文件都能一键复现同样的环境。
4. redis.conf 里真正需要改的那几项
4.1 从官方模板拷一份配置
官方镜像里其实内置了一份默认配置,但直接改容器内的文件不方便,正确做法是从官方仓库拿一份对应版本的redis.conf模板放到宿主机。拿到之后,大部分配置项保持默认即可,真正需要根据场景调整的其实就那么几项。下面这张表是我实际使用中改动频率最高的配置项:
| 配置项 | 默认值 | 建议值 | 调整理由 |
|---|---|---|---|
| bind | 127.0.0.1 | 0.0.0.0 | 容器内需监听所有网卡,否则宿主机连不进来 |
| protected-mode | yes | no | 配合 bind 调整,否则外部连接被拒 |
| requirepass | 空 | 自定义强密码 | 开发环境也建议设密码,避免误连 |
| appendonly | no | yes | 开启 AOF 持久化,降低数据丢失风险 |
| maxmemory | 0 | 按需设置 | 限制内存上限,防止容器吃满宿主机内存 |
| maxmemory-policy | noeviction | allkeys-lru | 内存满时的淘汰策略,按业务选 |
4.2 bind 与 protected-mode 的联动关系
这两个配置必须一起看。Redis 默认bind 127.0.0.1只监听本地回环,同时protected-mode yes会在没有密码且绑定非回环地址时拒绝外部连接。在容器场景下,容器内的 127.0.0.1 指的是容器自己,宿主机通过端口映射访问时,数据包是从虚拟机的网桥进来的,源地址不是容器回环,所以必须把bind改成0.0.0.0让 Redis 监听所有网卡。
但只改 bind 不改 protected-mode,如果又没设密码,Redis 会拒绝连接并给出警告。所以要么设密码,要么把 protected-mode 关掉。我的建议是设密码,哪怕只是本地开发,因为端口映射到宿主机后,同一局域网内的其他设备理论上也能访问到,裸奔的 Redis 风险不小。
4.3 持久化策略:RDB 与 AOF 怎么选
Redis 有两种持久化方式。RDB 是定时快照,把某一时刻的内存数据 dump 成二进制文件,恢复快、文件小,但两次快照之间的数据会丢。AOF 是追加写日志,每条写命令都记录,数据安全性高,但文件大、恢复慢。
开发环境我一般两个都开,或者至少开 AOF。配置上:
appendonly yes appendfsync everysecappendfsync everysec表示每秒刷一次盘,是性能和安全性的折中,也是官方推荐值。如果你追求极致安全可以设always,但每次写都刷盘性能会明显下降;设no则交给操作系统决定,性能最好但可能丢更多数据。这个取舍没有标准答案,看你能接受丢多少数据。
4.4 内存上限与淘汰策略
maxmemory默认是 0,表示不限制,这在容器里是危险的——Redis 可能把 Docker 虚拟机的内存吃满,导致其他容器或 Docker 本身出问题。我一般会设一个明确上限,比如maxmemory 512mb,同时配maxmemory-policy allkeys-lru,内存满了就按最近最少使用淘汰键。如果你的业务里有不能淘汰的关键数据,就得用volatile-lru只淘汰设了过期时间的键,或者干脆设大一点的上限并做好监控。
5. 连接验证与客户端接入
5.1 用 redis-cli 做基础连通性测试
容器起来后,最直接的验证方式是进容器里用自带的redis-cli:
docker exec -it redis-prod redis-cli进去之后执行ping返回PONG,再set foo bar、get foo验证读写正常。如果设了密码,进 cli 后要先auth 你的密码,或者启动时直接docker exec -it redis-prod redis-cli -a 你的密码。这里提醒一句,-a传密码会在命令历史里留下明文,正式环境更推荐进 cli 后再 auth,或者用环境变量方式传。
5.2 从 Mac 宿主机直连
更多时候我们是从宿主机的应用或工具直连。用redis-cli的话:
redis-cli -h 127.0.0.1 -p 6379 -a 你的密码如果你 Mac 上没装 redis-cli,也可以临时起一个客户端容器来连:
docker run -it --rm --network host redis:7.2-alpine redis-cli -h 127.0.0.1 -p 6379不过 Mac 上--network host的行为和 Linux 不同,它并不能真正共享宿主机网络,所以更稳妥的方式还是用端口映射加127.0.0.1直连。这一点是 Mac 上 Docker 网络模型的特殊性决定的,很多人从 Linux 迁移过来会在这里困惑。
5.3 应用侧连接串的写法
以常见的 Java 和 Python 客户端为例,连接串大致是这样:
import redis r = redis.Redis(host="127.0.0.1", port=6379, password="你的密码", decode_responses=True) print(r.ping())# Spring Boot application.properties spring.data.redis.host=127.0.0.1 spring.data.redis.port=6379 spring.data.redis.password=你的密码注意不同客户端对密码字段的命名不一样,有的叫password,有的叫auth,配置时对照官方文档确认。连接超时时间也建议显式设置,默认值有时偏长,出问题时等待体验很差。
6. 那些年我在 Mac 上踩过的坑
6.1 端口被占用导致容器起不来
最常见的报错是Bind for 0.0.0.0:6379 failed: port is already allocated。原因要么是本机之前装过 Redis 还在跑,要么是另一个容器占了这个端口。排查步骤:先lsof -i :6379看谁占着,如果是本机 Redis 服务,brew services stop redis停掉;如果是旧容器,docker ps找到它然后 stop 或 rm。改端口映射也是个办法,比如映射成6380:6379,宿主机连 6380 即可。
6.2 挂载配置文件变成目录的诡异现象
前面提过,宿主机上redis.conf不存在时,Docker 会创建一个同名目录挂载进去,容器启动时redis-server读到的不是文件而是目录,直接报错退出。这个坑的隐蔽之处在于docker ps -a能看到容器存在但状态是 Exited,docker logs里才看得到具体错误。解决办法很简单:启动前确认配置文件真实存在,用ls -l看一眼是普通文件而不是目录。
6.3 数据目录权限问题
有时候容器起来了但写数据报权限错误,日志里出现Permission denied之类。这通常是因为宿主机data目录的属主和容器内运行 Redis 的用户(官方镜像里是 redis 用户,UID 999)不匹配。解决办法是给目录放权,或者启动时加--user参数指定 UID。开发环境图省事可以直接chmod 777,但正式环境还是建议对齐 UID,别图一时方便留下隐患。
6.4 容器重启后数据"消失"的误判
有人反映重启容器后数据没了,其实多半是没挂载数据目录,或者挂载路径写错了。Redis 官方镜像的数据默认在/data,如果你挂载到了别的路径,RDB 和 AOF 文件就落在容器内部,容器一删自然全没。确认方法:进容器ls /data看有没有dump.rdb或appendonly.aof,再对比宿主机挂载目录里有没有对应文件。
6.5 内存不足导致容器被 kill
前面提过 Docker Desktop 给虚拟机分配的内存有限。如果你设了maxmemory但设得比虚拟机可用内存还大,或者同时跑了很多容器,Redis 可能被系统 OOM kill,表现为容器莫名其妙退出。排查看docker inspect里的OOMKilled字段,或者docker events观察。解决要么调大 Docker Desktop 的资源分配,要么把maxmemory设小一点,留出余量。
7. 日常维护与几个提效小习惯
7.1 常用运维命令清单
日常操作其实就那几个命令,整理成一张表备查:
| 操作 | 命令 |
|---|---|
| 查看运行中容器 | docker ps |
| 查看所有容器含已停止 | docker ps -a |
| 查看日志 | docker logs -f redis-prod |
| 进入容器 shell | docker exec -it redis-prod sh |
| 停止容器 | docker stop redis-prod |
| 启动容器 | docker start redis-prod |
| 删除容器 | docker rm -f redis-prod |
| 查看资源占用 | docker stats redis-prod |
docker stats这个命令我经常用,能实时看到容器的 CPU 和内存占用,调maxmemory的时候对着看很直观。
7.2 备份数据的一个简单做法
数据都在宿主机data目录里,备份其实就是拷文件。但直接拷运行中的 RDB 文件可能拷到写了一半的状态,稳妥做法是先触发一次保存再拷:
docker exec redis-prod redis-cli -a 你的密码 bgsave cp ~/docker/redis/data/dump.rdb ~/backup/dump-$(date +%Y%m%d).rdbbgsave是后台异步保存,不会阻塞主线程。拷完之后可以顺手清理一下旧备份,别让备份目录无限膨胀。
7.3 升级镜像版本的正确姿势
想升级 Redis 版本时,别直接docker pull新标签然后重建容器就完事,数据兼容性要先确认。正确顺序是:先备份数据,再拉新镜像,用新镜像起一个临时容器挂载同一份数据目录验证能否正常读取,确认没问题再替换正式容器。跨大版本升级(比如 6 升 7)尤其要谨慎,虽然 Redis 的 RDB 格式向后兼容做得不错,但 AOF 格式在某些版本间有变化,升级前查一下官方发布说明里的兼容性提示。
7.4 一个容易被忽视的习惯:给容器起有意义的名字
我见过太多人容器名都是随机生成的,时间一长docker ps里一堆nostalgic_hopper、fervent_mclean之类,根本分不清哪个是哪个项目的。养成起名习惯,比如redis-order-dev、redis-cache-test,一眼就知道用途。配合 compose 的container_name字段,团队协作时也不会撞名。
8. 关于安全与边界的一点个人体会
本地开发环境虽然不对外暴露,但端口映射到宿主机后,同一网络下的设备是能访问到的。我见过有人本地 Redis 裸奔,结果被同网段的扫描工具扫到,数据被清空的事故。所以哪怕是开发环境,我也坚持设密码、限制maxmemory、定期备份。另外,protected-mode和bind的配置不要图省事全放开,能收窄就收窄,比如只绑定到127.0.0.1配合端口映射,而不是0.0.0.0全开。
还有一点是资源边界。Docker Desktop 的资源分配是全局的,你给 Redis 设的maxmemory只是 Redis 自己的上限,不代表容器整体不会超。养成用docker stats观察的习惯,发现某个容器内存持续上涨就及时排查,别等到整台机器卡顿才反应过来。
这套流程我在几台不同芯片的 Mac 上反复验证过,Intel 和 Apple Silicon 在目录路径上有差异(主要是 Homebrew 前缀不同),但 Docker 层面的操作完全一致。真正需要花心思的不是命令本身,而是理解容器和宿主机之间的边界——网络怎么通、数据怎么落、资源怎么分。把这几个边界想清楚了,剩下的就是熟练度问题。