Python 与 Docker:多阶段构建、最小镜像与健康检查

文章目录

    • [1 镜像体积:被忽视的性能瓶颈](#1 镜像体积:被忽视的性能瓶颈)
    • [2 多阶段构建:体积从 1.2GB 到 80MB](#2 多阶段构建:体积从 1.2GB 到 80MB)
    • [3 基础镜像选型:slim 与 alpine 的权衡](#3 基础镜像选型:slim 与 alpine 的权衡)
    • [4 .dockerignore:从源头减少构建上下文](#4 .dockerignore:从源头减少构建上下文)
    • [5 安全底线:非 root 用户](#5 安全底线:非 root 用户)
    • [6 健康检查:让容器自我感知状态](#6 健康检查:让容器自我感知状态)
    • [7 信号处理与优雅退出](#7 信号处理与优雅退出)
    • [8 docker-compose 编排](#8 docker-compose 编排)
    • [9 镜像安全扫描](#9 镜像安全扫描)
    • [10 实战:图书管理 API 的完整 Docker 化](#10 实战:图书管理 API 的完整 Docker 化)
    • [11 总结](#11 总结)

1 镜像体积:被忽视的性能瓶颈

FROM python:3.12 在没有网络缓存的情况下首次拉取耗时可能超过 30 秒,镜像体积达到 900MB+------而其中 80% 的内容与实际运行的 Python 服务无关。镜像体积不是纯粹的美学问题,它直接影响三个生产指标:

python 复制代码
# 三个指标的数学关系
镜像体积 → 拉取时间 → 冷启动延迟
镜像体积 → 存储成本(容器注册表)
镜像体积 → 安全攻击面(CVEs 数量与镜像大小正相关)

以一个 1.2GB 的镜像为例:冷启动拉取耗时约 8 分钟(10Mbps 网络),而优化后的 80MB 镜像只需 30 秒。这个差距在 Auto Scaling 场景(每次扩容都需拉取镜像)中会被无限放大。

2 多阶段构建:体积从 1.2GB 到 80MB

多阶段构建利用 Docker 的分层缓存机制,将"构建依赖"和"运行时依赖"分离:

dockerfile 复制代码
# ── 阶段 1:构建器 ──────────────────────────────────────────
FROM python:3.12-slim AS builder

WORKDIR /build

# 只复制依赖文件,利用层缓存
COPY requirements.txt .
RUN pip install \
    --no-cache-dir \
    --target=/build/site-packages \
    -r requirements.txt

# ── 阶段 2:运行时(最终镜像)────────────────────────────────
FROM python:3.12-slim AS runtime

# 创建非 root 用户
RUN useradd --create-home --shell /bin/bash appuser

WORKDIR /home/appuser

# 只复制构建产物,不复制源码和构建工具
COPY --from=builder /build/site-packages ./site-packages

# 设置 PYTHONPATH,指向自定义 site-packages
ENV PYTHONPATH=/home/appuser/site-packages

# 复制源码
COPY --chown=appuser:appuser ./app ./app

USER appuser

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

EXPOSE 8000

CMD ["python", "-m", "app.main"]

关键优化点分析:

优化项 作用 效果
--target=/build/site-packages 不在运行时镜像中安装 pip,只复制编译好的包 体积 ↓ 50%
python:3.12-slim vs python:3.12 slim 不含开发工具链和文档 基础镜像 ↓ 70%
--chown=appuser:appuser 应用代码由非 root 用户拥有 安全加固
--no-cache-dir pip 不在镜像中缓存下载的 wheel 体积再 ↓ 10%

3 基础镜像选型:slim 与 alpine 的权衡

#mermaid-svg-3xtm9SJBtDQLVE0l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3xtm9SJBtDQLVE0l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3xtm9SJBtDQLVE0l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3xtm9SJBtDQLVE0l .error-icon{fill:#552222;}#mermaid-svg-3xtm9SJBtDQLVE0l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3xtm9SJBtDQLVE0l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3xtm9SJBtDQLVE0l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3xtm9SJBtDQLVE0l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3xtm9SJBtDQLVE0l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3xtm9SJBtDQLVE0l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3xtm9SJBtDQLVE0l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3xtm9SJBtDQLVE0l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3xtm9SJBtDQLVE0l .marker.cross{stroke:#333333;}#mermaid-svg-3xtm9SJBtDQLVE0l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3xtm9SJBtDQLVE0l p{margin:0;}#mermaid-svg-3xtm9SJBtDQLVE0l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3xtm9SJBtDQLVE0l .cluster-label text{fill:#333;}#mermaid-svg-3xtm9SJBtDQLVE0l .cluster-label span{color:#333;}#mermaid-svg-3xtm9SJBtDQLVE0l .cluster-label span p{background-color:transparent;}#mermaid-svg-3xtm9SJBtDQLVE0l .label text,#mermaid-svg-3xtm9SJBtDQLVE0l span{fill:#333;color:#333;}#mermaid-svg-3xtm9SJBtDQLVE0l .node rect,#mermaid-svg-3xtm9SJBtDQLVE0l .node circle,#mermaid-svg-3xtm9SJBtDQLVE0l .node ellipse,#mermaid-svg-3xtm9SJBtDQLVE0l .node polygon,#mermaid-svg-3xtm9SJBtDQLVE0l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3xtm9SJBtDQLVE0l .rough-node .label text,#mermaid-svg-3xtm9SJBtDQLVE0l .node .label text,#mermaid-svg-3xtm9SJBtDQLVE0l .image-shape .label,#mermaid-svg-3xtm9SJBtDQLVE0l .icon-shape .label{text-anchor:middle;}#mermaid-svg-3xtm9SJBtDQLVE0l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3xtm9SJBtDQLVE0l .rough-node .label,#mermaid-svg-3xtm9SJBtDQLVE0l .node .label,#mermaid-svg-3xtm9SJBtDQLVE0l .image-shape .label,#mermaid-svg-3xtm9SJBtDQLVE0l .icon-shape .label{text-align:center;}#mermaid-svg-3xtm9SJBtDQLVE0l .node.clickable{cursor:pointer;}#mermaid-svg-3xtm9SJBtDQLVE0l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3xtm9SJBtDQLVE0l .arrowheadPath{fill:#333333;}#mermaid-svg-3xtm9SJBtDQLVE0l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3xtm9SJBtDQLVE0l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3xtm9SJBtDQLVE0l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3xtm9SJBtDQLVE0l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3xtm9SJBtDQLVE0l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3xtm9SJBtDQLVE0l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3xtm9SJBtDQLVE0l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3xtm9SJBtDQLVE0l .cluster text{fill:#333;}#mermaid-svg-3xtm9SJBtDQLVE0l .cluster span{color:#333;}#mermaid-svg-3xtm9SJBtDQLVE0l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3xtm9SJBtDQLVE0l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3xtm9SJBtDQLVE0l rect.text{fill:none;stroke-width:0;}#mermaid-svg-3xtm9SJBtDQLVE0l .icon-shape,#mermaid-svg-3xtm9SJBtDQLVE0l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3xtm9SJBtDQLVE0l .icon-shape p,#mermaid-svg-3xtm9SJBtDQLVE0l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3xtm9SJBtDQLVE0l .icon-shape .label rect,#mermaid-svg-3xtm9SJBtDQLVE0l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3xtm9SJBtDQLVE0l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3xtm9SJBtDQLVE0l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3xtm9SJBtDQLVE0l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} FROM python:3.12
镜像体积: ~900MB
FROM python:3.12-slim
镜像体积: ~150MB

基础系统: Debian

兼容性: ✅ 最佳
FROM python:3.12-alpine
镜像体积: ~55MB

基础系统: Alpine musl

兼容性: ⚠️ 三个已知坑
坑1: musl 与 glibc 不兼容

PostgreSQL/MySQL 客户端库

多线程库可能异常
坑2: 无 gcc make

安装 C 扩展包(如 lxml,

Pillow, cryptography)失败
坑3: Alpine 默认时区 UTC

涉及时间计算时需手动

安装 tzdata
推荐用于生产
绕过: 使用 slim 或手动装 glibc
绕过: apk add gcc g++ musl-dev python3-dev libffi-dev
绕过: apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

alpine 适用场景 :对体积极端敏感(边缘计算、IoT 设备)、且依赖的 Python 包全部为纯 Python 实现(无 C 扩展)。大多数情况下,python:3.12-slim 是更安全的选择。

4 .dockerignore:从源头减少构建上下文

docker build 会将构建上下文(Build Context,当前目录及其子目录)打包发送给 Docker daemon。上下文过大是构建卡顿的常见原因:

gitignore 复制代码
# .dockerignore --- 与 .gitignore 类似的排除规则
# Git 仓库(通常数十 MB 到数百 MB)
.git
.gitignore

# Python 字节码
__pycache__
*.py[co]
*.egg-info
.eggs

# 虚拟环境
venv/
env/
.venv/

# 测试与文档
tests/
docs/
*.md

# Node.js(如果有前端)
node_modules/

# IDE 配置
.vscode/
.idea/
*.swp

# Docker 相关(避免递归包含)
Dockerfile
docker-compose*.yml

# 大文件
*.mp4
*.zip
*.tar.gz
data/
*.csv
*.parquet

一个常见误区是"只排除 .git 就够了"------实际上 __pycache__.venv.pytest_cache 这些目录在本地开发时积累很快,如果不排除,每次构建都会将它们打包发送。

5 安全底线:非 root 用户

默认情况下,容器以 root 身份运行。如果容器存在漏洞被攻破,攻击者获得的就是主机上的 root 权限:

dockerfile 复制代码
# 错误示范:容器以 root 运行
FROM python:3.12
COPY . .
CMD ["python", "app.py"]  # root 运行

# 正确示范:切换到普通用户
FROM python:3.12-slim
RUN useradd --create-home --shell /bin/bash --uid 1000 appuser
COPY --chown=appuser:appuser . /home/appuser/app
USER appuser
CMD ["python", "/home/appuser/app/app.py"]

注意 :服务端口号小于 1024(如 80、443)时,需要 root 权限绑定。此时可以在 Dockerfile 中使用 gosusu-exec 工具来"临时提升到 root 执行绑定操作,再降回普通用户"。

6 健康检查:让容器自我感知状态

HEALTHCHECK 指令告诉 Docker 如何判断容器是否"健康":

dockerfile 复制代码
HEALTHCHECK --interval=30s \
            --timeout=5s \
            --start-period=30s \
            --retries=3 \
    CMD python -c "
import urllib.request
import os

try:
    # 基础连通性
    resp = urllib.request.urlopen('http://localhost:8000/health', timeout=3)
    if resp.status != 200:
        exit(1)

    # 深度检查:数据库连通性
    import psycopg2
    conn = psycopg2.connect(os.environ['DATABASE_URL'])
    conn.close()
except Exception:
    exit(1)
"

/health 端点的实现不应只返回 HTTP 200------它应该检查下游依赖(数据库、Redis、外部 API),并在依赖不可用时返回非 200 状态码或主动退出:

python 复制代码
# app/api/health.py
from fastapi import APIRouter, Response
import psycopg2, redis

router = APIRouter()

@router.get("/health")
def health_check(response: Response):
    checks = {}

    # 1. 数据库检查
    try:
        conn = psycopg2.connect(os.environ["DATABASE_URL"])
        conn.close()
        checks["database"] = "ok"
    except Exception as e:
        checks["database"] = f"error: {e}"
        response.status_code = 503  # Service Unavailable

    # 2. Redis 检查
    try:
        r = redis.from_url(os.environ["REDIS_URL"])
        r.ping()
        checks["redis"] = "ok"
    except Exception as e:
        checks["redis"] = f"error: {e}"
        response.status_code = 503

    return {"status": "ok" if response.status_code == 200 else "degraded", "checks": checks}

7 信号处理与优雅退出

当执行 docker stop 时,Docker 向容器内的 PID 1 进程发送 SIGTERM 信号。默认等待 10 秒后若进程未退出,则发送 SIGKILL 强制终止。如果 Python 进程收到 SIGTERM 时没有正确关闭数据库连接,就会产生"连接未正确归还连接池"的警告:

python 复制代码
# app/main.py --- FastAPI 优雅退出
from contextlib import asynccontextmanager
import asyncio

@asynccontextmanager
async def lifespan(app):
    # 启动时:预热连接池
    await db_pool.initialize()
    redis_client = await aioredis.connect(REDIS_URL)
    yield
    # 关闭时:等待中的请求继续处理,然后关闭资源
    await asyncio.sleep(0.5)  # 给在途请求 0.5 秒的缓冲时间
    await db_pool.close()
    await redis_client.close()

app = FastAPI(lifespan=lifespan)

对应的 Dockerfile 需要确保容器等待时间足够:

dockerfile 复制代码
# 给予容器充足的优雅退出时间(默认 10s)
STOPSIGNAL SIGTERM

# docker-compose.yml 中可覆盖
# stop_grace_period: 30s

8 docker-compose 编排

对于本地开发和测试环境,docker-compose.yml 提供了声明式的多服务编排:

yaml 复制代码
# docker-compose.yml
version: "3.9"

services:
  # ── Python API 服务 ────────────────────────────────────
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: runtime
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://postgres:secret@db:5432/appdb
      - REDIS_URL=redis://cache:6379/0
    depends_on:
      db:
        condition: service_healthy  # 等待数据库健康后再启动
      cache:
        condition: service_started  # 只等待 Redis 启动
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 30s
      timeout: 5s
      retries: 3
    restart: unless-stopped

  # ── PostgreSQL 数据库 ──────────────────────────────────
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # ── Redis 缓存 ──────────────────────────────────────────
  cache:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

关键配置说明:

  • depends_on + condition: service_healthy:数据库健康检查通过后才启动 API,避免 API 启动时数据库尚未就绪导致的连接失败。
  • target: runtime:在多阶段构建中只构建 runtime 阶段,跳过 builder 阶段,减少构建产物暴露。
  • restart: unless-stopped:容器退出后自动重启,但手动 docker stop 时不重启------这是生产环境的标准配置。

9 镜像安全扫描

镜像构建完成后,使用 trivy 扫描已知 CVE:

bash 复制代码
# 安装 trivy
brew install trivy   # macOS
# 或
wget https://github.com/aquasecurity/trivy/releases/latest/download/trivy_Linux-64bit.tar.gz

# 扫描镜像(推荐在 CI/CD 中运行)
trivy image --severity HIGH,CRITICAL \
    --exit-code 1 \
    --ignore-unfixed \
    myregistry.io/app-api:latest

# 扫描结果示例:
# python:3.12-slim (debian 12.5)
# ==========================
# Total: 47 (HIGH: 12, CRITICAL: 3)
#
# CRITICAL: CVE-2024-XXXX  openssl  < 3.0.13 -> 升级基础镜像到 python:3.12.1
# CRITICAL: CVE-2024-YYYY  libssl  < 3.0.16 -> 同上

常见的 CVE 修复策略:

CVE 类型 修复方式
基础镜像系统漏洞 升级 python:3.12-slim 到最新小版本
Python 包漏洞 pip-audit 扫描依赖,pip install --upgrade 修复
已知无法修复的漏洞 trivy ignore --policy trivy-policy.yaml 文档化忽略原因

10 实战:图书管理 API 的完整 Docker 化

将 Python 实战系列 #01 的图书管理 API(FastAPI + SQLAlchemy + PostgreSQL)完整 Docker 化:

dockerfile 复制代码
# Dockerfile
FROM python:3.12-slim AS builder

WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/build/site-packages -r requirements.txt

FROM python:3.12-slim AS runtime

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    && rm -rf /var/lib/apt/lists/* \
    && useradd --create-home --shell /bin/bash appuser

WORKDIR /home/appuser
ENV PYTHONPATH=/home/appuser/site-packages

COPY --from=builder /build/site-packages ./site-packages
COPY --chown=appuser:appuser ./app ./app
COPY --chown=appuser:appuser ./alembic ./alembic

USER appuser

HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

EXPOSE 8000
CMD ["python", "-m", "app.main"]

对应的 docker-compose.yml 启动完整技术栈:

bash 复制代码
# 启动所有服务
docker compose up --build -d

# 查看服务状态
docker compose ps

# 查看 API 日志
docker compose logs -f api

# 初始化数据库(首次运行)
docker compose exec api alembic upgrade head

# 压测验证
docker compose exec api python -c "
import httpx, time
for i in range(100):
    r = httpx.get('http://localhost:8000/books?page=1&page_size=10')
    print(f'Status: {r.status_code}, Time: {r.elapsed.total_seconds()*1000:.1f}ms')
"

#mermaid-svg-66oymbUjLOlPDjvN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-66oymbUjLOlPDjvN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-66oymbUjLOlPDjvN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-66oymbUjLOlPDjvN .error-icon{fill:#552222;}#mermaid-svg-66oymbUjLOlPDjvN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-66oymbUjLOlPDjvN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-66oymbUjLOlPDjvN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-66oymbUjLOlPDjvN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-66oymbUjLOlPDjvN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-66oymbUjLOlPDjvN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-66oymbUjLOlPDjvN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-66oymbUjLOlPDjvN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-66oymbUjLOlPDjvN .marker.cross{stroke:#333333;}#mermaid-svg-66oymbUjLOlPDjvN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-66oymbUjLOlPDjvN p{margin:0;}#mermaid-svg-66oymbUjLOlPDjvN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-66oymbUjLOlPDjvN .cluster-label text{fill:#333;}#mermaid-svg-66oymbUjLOlPDjvN .cluster-label span{color:#333;}#mermaid-svg-66oymbUjLOlPDjvN .cluster-label span p{background-color:transparent;}#mermaid-svg-66oymbUjLOlPDjvN .label text,#mermaid-svg-66oymbUjLOlPDjvN span{fill:#333;color:#333;}#mermaid-svg-66oymbUjLOlPDjvN .node rect,#mermaid-svg-66oymbUjLOlPDjvN .node circle,#mermaid-svg-66oymbUjLOlPDjvN .node ellipse,#mermaid-svg-66oymbUjLOlPDjvN .node polygon,#mermaid-svg-66oymbUjLOlPDjvN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-66oymbUjLOlPDjvN .rough-node .label text,#mermaid-svg-66oymbUjLOlPDjvN .node .label text,#mermaid-svg-66oymbUjLOlPDjvN .image-shape .label,#mermaid-svg-66oymbUjLOlPDjvN .icon-shape .label{text-anchor:middle;}#mermaid-svg-66oymbUjLOlPDjvN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-66oymbUjLOlPDjvN .rough-node .label,#mermaid-svg-66oymbUjLOlPDjvN .node .label,#mermaid-svg-66oymbUjLOlPDjvN .image-shape .label,#mermaid-svg-66oymbUjLOlPDjvN .icon-shape .label{text-align:center;}#mermaid-svg-66oymbUjLOlPDjvN .node.clickable{cursor:pointer;}#mermaid-svg-66oymbUjLOlPDjvN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-66oymbUjLOlPDjvN .arrowheadPath{fill:#333333;}#mermaid-svg-66oymbUjLOlPDjvN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-66oymbUjLOlPDjvN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-66oymbUjLOlPDjvN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-66oymbUjLOlPDjvN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-66oymbUjLOlPDjvN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-66oymbUjLOlPDjvN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-66oymbUjLOlPDjvN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-66oymbUjLOlPDjvN .cluster text{fill:#333;}#mermaid-svg-66oymbUjLOlPDjvN .cluster span{color:#333;}#mermaid-svg-66oymbUjLOlPDjvN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-66oymbUjLOlPDjvN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-66oymbUjLOlPDjvN rect.text{fill:none;stroke-width:0;}#mermaid-svg-66oymbUjLOlPDjvN .icon-shape,#mermaid-svg-66oymbUjLOlPDjvN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-66oymbUjLOlPDjvN .icon-shape p,#mermaid-svg-66oymbUjLOlPDjvN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-66oymbUjLOlPDjvN .icon-shape .label rect,#mermaid-svg-66oymbUjLOlPDjvN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-66oymbUjLOlPDjvN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-66oymbUjLOlPDjvN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-66oymbUjLOlPDjvN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 运行时阶段 (runtime)
构建阶段 (builder)
docker-compose 编排
service_healthy
api 服务

(FastAPI)
db 服务

(PostgreSQL)
cache 服务

(Redis)
requirements.txt
pip install

--target=/build/site-packages
编译好的 site-packages
python:3.12-slim

(~150MB)
COPY --from=builder

/build/site-packages
appuser 用户

非 root
EXPOSE 8000

HEALTHCHECK

11 总结

Python 服务的 Docker 化不是简单地把代码塞进容器------它涉及镜像体积优化、安全加固、健康检查、优雅退出等多个工程维度。

优化维度 关键做法 量化效果
镜像体积 多阶段构建 + slim 基础镜像 1.2GB → 80MB
安全性 非 root 用户 + 镜像 CVE 扫描 消除容器逃逸 root 风险
可观测性 健康检查端点(DB+Redis 深度检查) K8s 自动发现不健康 Pod
启动顺序 depends_on + healthcheck condition 避免连接失败 race condition
优雅退出 SIGTERM 处理 + lifespan 上下文 连接池正确关闭,无泄漏

扩展阅读

如果本文对你有帮助,欢迎点赞、关注!

相关推荐
变量未定义~1 小时前
快速幂、费马小定理、约数的个数、欧拉函数模板、矩阵快速幂
开发语言
hyunbar1 小时前
NOT IN 的 NULL 陷阱:一次 UNION 数据“神秘消失“
开发语言·sql
C+++Python1 小时前
如何在 Java 中使用 BIO、NIO 和 AIO?
java·开发语言·nio
左心房的默白,,,1 小时前
17:FDC数据采集与数据分析基础(EAP进阶)
运维·数据分析·自动化
小猿姐1 小时前
三种 MongoDB Operator 实测对比:Community、Percona 与 KubeBlocks,谁更适合团队落地?
运维·mongodb·kubernetes
哈泽尔都1 小时前
运动控制教学——5分钟学会力控算法(阻抗/导纳/力位混合)
c++·python·算法·决策树·贪心算法·机器人·gpu算力
情绪总是阴雨天~1 小时前
Dockerfile 完全指南:从指令详解到实战构建
docker
189228048611 小时前
NV022固态MT29F16T08GWLCEM5-QBES:C
c语言·开发语言
月疯1 小时前
PyTorch 中定义了一个 LeakyReLU 激活函数层
人工智能·pytorch·python