第1章:Python应用容器化——构建轻量级Python镜像

容器化这件事,说白了就是把你的Python应用连同它的运行环境打包成一个标准化的「盒子」。这个盒子拿到哪里都能跑,开发机、测试服务器、生产环境,行为完全一致。我刚开始做容器化时,总觉得不就是写个Dockerfile嘛,结果踩了不少坑。今天咱们就聊聊怎么把这个盒子做得又小又稳。

1.1 基础镜像的选择:Alpine vs Slim

选基础镜像,是容器化的第一步。也是很多人容易忽视的一步。

Python官方提供了好几种基础镜像。最常用的两个是python:3.11-slimpython:3.11-alpine。它们有什么区别?

特性 python:3.11-slim python:3.11-alpine
基础系统 Debian Slim(精简版) Alpine Linux(基于musl libc)
镜像大小 约120MB 约50MB
包管理器 apt apk
C库 glibc musl
兼容性 高,几乎无坑 部分C扩展需要额外处理

我个人习惯,生产环境优先用slim。为什么?

Alpine虽然小,但它的C库是musl而不是glibc。很多Python的C扩展(比如psycopg2numpypandas)在编译时默认依赖glibc。你想想看,在Alpine上跑这些库,要么得装一堆编译工具重新编译,要么就得忍受莫名其妙的段错误。

我曾经在Alpine上部署一个Django项目,本地测试一切正常,上了预发布环境就疯狂报错。查了半天,发现是psycopg2在musl环境下有内存对齐问题。从那以后,非必要不用Alpine。

当然,如果你的应用纯Python,没有任何C扩展依赖,那Alpine确实是个好选择。50MB的镜像,拉取快,启动也快。

1.2 多阶段构建:把镜像「瘦身」到底

多阶段构建,是Docker提供的一个杀手锏。它的核心思想很简单:编译环境和运行环境分开

你想想看,编译Python的C扩展需要gcc、python-dev、各种头文件。但这些在运行时完全用不到。把它们留在最终镜像里,纯粹是浪费空间。

来看一个实际例子。假设我们要容器化一个FastAPI应用,依赖了orjson(一个高性能的JSON库,需要编译C扩展)。

# 第一阶段:编译环境
FROM python:3.11-slim AS builder

WORKDIR /app

# 安装编译依赖
RUN apt-get update && apt-get install -y \
    gcc \
    python3-dev \
    && rm -rf /var/lib/apt/lists/*

# 复制依赖文件
COPY Pipfile Pipfile.lock ./

# 安装依赖(包括编译C扩展)
RUN pip install pipenv && pipenv install --system --deploy

# 第二阶段:运行环境
FROM python:3.11-slim

WORKDIR /app

# 只复制编译好的依赖,不复制编译工具
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# 复制应用代码
COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

这个Dockerfile里,第一阶段装了gcc和头文件,编译完所有依赖。第二阶段只复制编译好的产物。最终镜像里没有gcc,没有头文件,干干净净。

小技巧:多阶段构建时,可以用--from=builder精确控制复制哪些文件。别一股脑全复制过来,那样就失去多阶段的意义了。

我记得有一次,一个同事的镜像从800MB优化到120MB,就是靠多阶段构建。他当时特别兴奋,说「原来之前800MB里,有600MB都是编译工具和缓存」。

1.3 依赖管理:Pipenv vs Poetry

依赖管理,是Python容器化里最容易出问题的地方。我见过太多人直接在Dockerfile里写pip install -r requirements.txt,然后祈祷别出问题。

说实话,requirements.txt太简陋了。它不记录依赖的依赖,也不做版本锁定。今天能装,明天可能就装不上。

我个人推荐两个工具:PipenvPoetry。它们都解决了依赖锁定和虚拟环境管理的问题。

Pipenv方案

FROM python:3.11-slim AS builder

WORKDIR /app

# 安装Pipenv
RUN pip install pipenv

# 复制依赖文件
COPY Pipfile Pipfile.lock ./

# 安装依赖
RUN pipenv install --system --deploy

# ... 后续阶段

Pipenv的Pipfile.lock会锁定所有依赖的精确版本。包括依赖的依赖。这样就能保证每次构建的依赖环境完全一致。

Poetry方案

FROM python:3.11-slim AS builder

WORKDIR /app

# 安装Poetry
RUN pip install poetry

# 复制依赖文件
COPY pyproject.toml poetry.lock ./

# 配置Poetry不创建虚拟环境(容器里不需要)
RUN poetry config virtualenvs.create false

# 安装依赖
RUN poetry install --no-dev --no-interaction --no-ansi

# ... 后续阶段

Poetry比Pipenv更现代一些。它的依赖解析速度更快,而且pyproject.toml是Python社区的标准格式。

我的建议:新项目用Poetry,老项目如果已经在用Pipenv就别折腾了。两个都能用,别在工具选择上浪费时间。

1.4 环境变量配置:12-Factor App的实践

容器化应用的环境变量配置,遵循的是12-Factor App的第三因子:把配置存到环境变量里。

为什么?因为环境变量是平台无关的。在开发机上是.env文件,在Kubernetes里是ConfigMap,在Docker Compose里是environment字段。代码不需要改,换套环境变量就行。

来看一个实际的Dockerfile示例:

FROM python:3.11-slim

WORKDIR /app

# 复制依赖和代码
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY . .

# 设置默认环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    APP_ENV=production \
    LOG_LEVEL=info

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

这里有几个关键点:

  • PYTHONUNBUFFERED=1:让Python日志实时输出,不会因为缓冲而丢失日志
  • PYTHONDONTWRITEBYTECODE=1:不生成.pyc文件,减少镜像体积
  • APP_ENVLOG_LEVEL:应用级别的配置,运行时可以覆盖
注意:不要把敏感信息(数据库密码、API密钥)写在Dockerfile里。Dockerfile是版本控制的,密码不应该进Git。用Docker Compose的environment字段或者Kubernetes的Secret来注入。

在Python代码里读取环境变量,我习惯用pydantic-settings

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_env: str = "development"
    log_level: str = "info"
    database_url: str
    redis_url: str

    class Config:
        env_file = ".env"

settings = Settings()

这样写的好处是:类型安全、自动从环境变量读取、支持.env文件。我在好几个项目里都用这个方案,没出过问题。

1.5 实战:一个完整的Dockerfile

最后,咱们把上面讲的东西串起来,写一个完整的Dockerfile。这个Dockerfile我实际用在生产环境里,效果不错。

# ========== 第一阶段:编译依赖 ==========
FROM python:3.11-slim AS builder

WORKDIR /app

# 安装编译工具
RUN apt-get update && apt-get install -y \
    gcc \
    python3-dev \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# 安装Poetry
RUN pip install poetry

# 复制依赖文件
COPY pyproject.toml poetry.lock ./

# 安装生产依赖
RUN poetry config virtualenvs.create false \
    && poetry install --no-dev --no-interaction --no-ansi

# ========== 第二阶段:运行环境 ==========
FROM python:3.11-slim

WORKDIR /app

# 安装运行时系统依赖
RUN apt-get update && apt-get install -y \
    libpq5 \
    && rm -rf /var/lib/apt/lists/*

# 复制编译好的依赖
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# 复制应用代码
COPY . .

# 环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    APP_ENV=production

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

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

这个Dockerfile有几个亮点:

  • 多阶段构建,最终镜像只有150MB左右
  • 只安装运行时需要的系统库(libpq5),不装编译工具
  • 加了健康检查,方便Kubernetes做存活探针
  • 环境变量统一管理,运行时可以覆盖

嗯,到这里,Python应用容器化的核心内容就讲完了。下一章咱们聊聊镜像仓库的管理和CI/CD集成。到时候见。