在 Docker 中跑 Redis Sentinel,並用 FastAPI 連線的完整指南
問題背景
本地開發常見的場景:Redis Sentinel 和 Redis 跑在 Docker 裡,FastAPI 在 host 上開發。
起好服務之後,FastAPI 連 sentinel 詢問 master 在哪,卻收到 172.21.0.2:6379——
這是 Docker 的內部 IP,host 上根本連不到。
[FastAPI on host] ──ask sentinel──► sentinel 回 172.21.0.2:6379
↓
FastAPI 連不到!根本原因
Sentinel 啟動時做了這件事:
sentinel monitor mymaster redis-master 6379 2redis-master 在 Docker 網路中被解析成 172.21.0.2。
Sentinel 記住這個 IP,之後有 client 詢問 master 在哪,就回傳 172.21.0.2:6379。
為什麼這個問題沒有簡單的 workaround?
讓 sentinel 回傳一個 host 可以連到的地址,需要滿足兩個互相矛盾的條件:
| 需求 | 地址 |
|---|---|
| Sentinel(在 Docker 裡)要能連到 master | Docker 內部 IP(如 172.21.0.2) |
| FastAPI(在 host 上)要能連到 master | 127.0.0.1 或其他 host 可達地址 |
沒有任何 Docker 原生機制能讓同一個地址同時滿足這兩個條件。
常見的「解法」及其問題:
- 在 FastAPI 把 Docker IP remap 成
127.0.0.1:dev 和 production 行為不一致 - 用
host.docker.internal:在 host 機器上無法解析(macOS 也不行) - 改
/etc/hosts:需要 admin 權限,每台開發機都要設定 - 兩次 failover 強制讓 sentinel 學到
127.0.0.1:replica 宣告127.0.0.1後,sentinel 從內部試圖連127.0.0.1(容器本身的 localhost),連不到,replica 被標為s_down,failover 失敗
正確的解法
讓 FastAPI 也跑在 Docker 裡。
這不只是 workaround,而是更正確的架構:
- FastAPI 和 Redis 在同一個 Docker network,sentinel 回傳的 Docker 內部 IP 完全可達
- 程式碼和 production 完全一致,零差異
- 用 volume mount 做 hot reload,開發體驗和直接跑在 host 幾乎相同
- 這也是大多數 production 環境的實際部署方式
目錄結構
redis-sentinel/
├── docker-compose.yml
├── Dockerfile
├── main.py
└── requirements.txtdocker-compose.yml
services:
redis-master:
image: bitnami/redis:latest
environment:
- REDIS_REPLICATION_MODE=master
- ALLOW_EMPTY_PASSWORD=yes
redis-replica:
image: bitnami/redis:latest
environment:
- REDIS_REPLICATION_MODE=slave
- REDIS_MASTER_HOST=redis-master
- REDIS_MASTER_PORT_NUMBER=6379
- ALLOW_EMPTY_PASSWORD=yes
depends_on:
- redis-master
sentinel-1:
image: bitnami/redis-sentinel:latest
environment:
- REDIS_SENTINEL_PORT_NUMBER=26379
- REDIS_MASTER_HOST=redis-master
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_SET=mymaster
- REDIS_SENTINEL_QUORUM=2
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
- REDIS_SENTINEL_FAILOVER_TIMEOUT=10000
depends_on:
- redis-master
- redis-replica
sentinel-2:
image: bitnami/redis-sentinel:latest
environment:
- REDIS_SENTINEL_PORT_NUMBER=26380
- REDIS_MASTER_HOST=redis-master
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_SET=mymaster
- REDIS_SENTINEL_QUORUM=2
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
- REDIS_SENTINEL_FAILOVER_TIMEOUT=10000
depends_on:
- redis-master
- redis-replica
sentinel-3:
image: bitnami/redis-sentinel:latest
environment:
- REDIS_SENTINEL_PORT_NUMBER=26381
- REDIS_MASTER_HOST=redis-master
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_SET=mymaster
- REDIS_SENTINEL_QUORUM=2
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
- REDIS_SENTINEL_FAILOVER_TIMEOUT=10000
depends_on:
- redis-master
- redis-replica
fastapi:
build: .
ports:
- "8000:8000"
volumes:
- .:/app # hot reload — 修改程式碼立即生效
depends_on:
- sentinel-1
- sentinel-2
- sentinel-3Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]FastAPI Demo(main.py)
注意:sentinel 地址直接用 service name,不需要任何 workaround。
from fastapi import FastAPI, HTTPException
from redis.sentinel import Sentinel
from redis import Redis
sentinel = Sentinel(
[
("sentinel-1", 26379),
("sentinel-2", 26380),
("sentinel-3", 26381),
],
socket_timeout=1,
)
app = FastAPI()
def master() -> Redis:
return sentinel.master_for("mymaster", socket_timeout=1, decode_responses=True)
@app.get("/health")
def health():
try:
host, port = sentinel.discover_master("mymaster")
master().ping()
return {"status": "ok", "master": f"{host}:{port}"}
except Exception as e:
raise HTTPException(status_code=503, detail=str(e))
@app.post("/set/{key}/{value}")
def set_key(key: str, value: str):
master().set(key, value)
return {"key": key, "value": value}
@app.get("/get/{key}")
def get_key(key: str):
value = master().get(key)
return {"key": key, "value": value}requirements.txt
fastapi
uvicorn
redis啟動步驟
docker compose up -d --build第一次會 build FastAPI image,之後修改程式碼直接生效(volume mount + --reload)。
驗證結果
# 健康檢查
curl http://localhost:8000/health
# {"status":"ok","master":"172.21.0.2:6379"}
# 寫入
curl -X POST http://localhost:8000/set/hello/world
# {"key":"hello","value":"world"}
# 讀取
curl http://localhost:8000/get/hello
# {"key":"hello","value":"world"}master 顯示 172.21.0.2:6379(Docker 內部 IP),FastAPI 在 Docker 裡完全可達。
為什麼不能用 127.0.0.1 trick?
這個問題被問了很多次,常見的嘗試:
嘗試一:replica 宣告 127.0.0.1
設定 REDIS_REPLICA_IP=127.0.0.1,讓 sentinel 記住 replica 是 127.0.0.1。
問題:sentinel 在 Docker 裡,它試圖連 127.0.0.1:6380——也就是 sentinel 容器本身的 localhost。連不到,replica 被標為 s_down,failover 失敗。
嘗試二:host.docker.internal
設定 REDIS_MASTER_HOST=host.docker.internal 讓 sentinel 透過 host 的 port mapping 連 master。Sentinel 回傳 host.docker.internal:6379。
問題:host.docker.internal 是讓容器連到 host 用的 DNS,在 host 機器上不能解析。FastAPI 拿到 host.docker.internal 之後無法建立連線。
嘗試三:兩次 failover 強制讓 sentinel 學到 127.0.0.1
先讓 replica 宣告 127.0.0.1:6380,觸發 failover,replica 升為 master,sentinel 記住新 master 是 127.0.0.1:6380。
問題:見「嘗試一」。Replica 宣告 127.0.0.1 之後,sentinel 連不到它,s_down,無法被選為 failover 目標。
Bitnami 相關 env var 說明
| 變數 | 服務 | 說明 |
|---|---|---|
REDIS_REPLICATION_MODE | redis | master 或 slave |
REDIS_MASTER_HOST | redis (slave) / sentinel | master 的 hostname |
REDIS_MASTER_PORT_NUMBER | redis (slave) / sentinel | master 的 port |
REDIS_MASTER_SET | sentinel | sentinel 監控的 master 名稱 |
REDIS_SENTINEL_QUORUM | sentinel | 需要幾個 sentinel 同意才能 failover |
REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS | sentinel | 幾 ms 沒回應就標為 down |
REDIS_SENTINEL_FAILOVER_TIMEOUT | sentinel | failover 的 timeout |