Skip to content

在 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 2

redis-master 在 Docker 網路中被解析成 172.21.0.2。 Sentinel 記住這個 IP,之後有 client 詢問 master 在哪,就回傳 172.21.0.2:6379

為什麼這個問題沒有簡單的 workaround?

讓 sentinel 回傳一個 host 可以連到的地址,需要滿足兩個互相矛盾的條件:

需求地址
Sentinel(在 Docker 裡)要能連到 masterDocker 內部 IP(如 172.21.0.2
FastAPI(在 host 上)要能連到 master127.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.txt

docker-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-3

Dockerfile

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_MODEredismasterslave
REDIS_MASTER_HOSTredis (slave) / sentinelmaster 的 hostname
REDIS_MASTER_PORT_NUMBERredis (slave) / sentinelmaster 的 port
REDIS_MASTER_SETsentinelsentinel 監控的 master 名稱
REDIS_SENTINEL_QUORUMsentinel需要幾個 sentinel 同意才能 failover
REDIS_SENTINEL_DOWN_AFTER_MILLISECONDSsentinel幾 ms 沒回應就標為 down
REDIS_SENTINEL_FAILOVER_TIMEOUTsentinelfailover 的 timeout

延伸閱讀