部署一个 Python + Redis + Nginx 的服务,容器启动顺序搞错了。Redis 还没 ready,Python 服务就报 Connection refused。排查了半天,发现是 depends_on 只保证容器启动顺序,不保证服务就绪。

问题现象

# docker-compose.yml
services:
  web:
    build: .
    depends_on:
      - redis
    ports:
      - "8000:8000"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
docker-compose up -d

Python 服务启动后报错:

redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. Connection refused.

坑在于:depends_on 只保证 Redis 容器先启动,不保证 Redis 服务已经就绪。Redis 容器启动了,但 Redis 服务可能还在初始化。

解决方案

方案一:healthcheck + condition(推荐)

services:
  web:
    build: .
    depends_on:
      redis:
        condition: service_healthy
    ports:
      - "8000:8000"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

healthcheck 定义健康检查命令,condition: service_healthy 等待 Redis 健康后再启动 web。

方案二:启动脚本等待

# app.py
import redis
import time

def wait_for_redis(host, port, max_retries=30):
    """等待 Redis 就绪"""
    for i in range(max_retries):
        try:
            r = redis.Redis(host=host, port=port)
            r.ping()
            print("Redis 已就绪")
            return r
        except redis.ConnectionError:
            print(f"等待 Redis... ({i+1}/{max_retries})")
            time.sleep(1)
    raise Exception("Redis 连接超时")

r = wait_for_redis("redis", 6379)

坑在于:这个方案需要在应用代码里加等待逻辑,不够优雅。

方案三:wait-for-it 脚本

services:
  web:
    build: .
    depends_on:
      - redis
    command: >
      sh -c "wait-for-it -t 30 redis:6379 -- python app.py"
    ports:
      - "8000:8000"
# Dockerfile
FROM python:3.11-slim

# 安装 wait-for-it
RUN apt-get update && apt-get install -y wait-for-it && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

这个方案需要在镜像里安装 wait-for-it,增加镜像体积。

完整配置示例

Python + Redis + Nginx

services:
  web:
    build: .
    depends_on:
      redis:
        condition: service_healthy
    environment:
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    ports:
      - "8000:8000"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      web:
        condition: service_started

volumes:
  redis_data:

健康检查配置详解

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  interval: 10s      # 检查间隔
  timeout: 5s        # 超时时间
  retries: 3         # 重试次数
  start_period: 30s  # 启动等待时间

start_period 给容器启动的时间,这段时间内的失败不算在 retries 里。

网络配置

services:
  web:
    build: .
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

同一个网络里的容器可以用服务名互相访问,比如 redis:6379

常用命令

# 启动所有服务
docker-compose up -d

# 查看服务状态
docker-compose ps

# 查看日志
docker-compose logs -f web

# 重建并启动
docker-compose up -d --build

# 停止并清理
docker-compose down

# 停止并清理数据卷
docker-compose down -v

踩坑总结

  1. depends_on 只保证容器启动顺序,不保证服务就绪
  2. healthcheck + condition: service_healthy 等待服务健康
  3. start_period 给容器启动的时间,避免误判
  4. 同一个网络里的容器可以用服务名互相访问
  5. 生产环境用 docker-compose up -d --build 重建镜像

从"容器启动了但服务没就绪"到"服务健康后再启动依赖",只需要配置 healthcheck + condition。坑在于:很多人只用 depends_on,不知道还有 condition 这个选项。

标签: docker, 部署

添加新评论