第1章:Python应用容器化——构建轻量级Python镜像
容器化这件事,说白了就是把你的Python应用连同它的运行环境打包成一个标准化的「盒子」。这个盒子拿到哪里都能跑,开发机、测试服务器、生产环境,行为完全一致。我刚开始做容器化时,总觉得不就是写个Dockerfile嘛,结果踩了不少坑。今天咱们就聊聊怎么把这个盒子做得又小又稳。
1.1 基础镜像的选择:Alpine vs Slim
选基础镜像,是容器化的第一步。也是很多人容易忽视的一步。
Python官方提供了好几种基础镜像。最常用的两个是python:3.11-slim和python: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扩展(比如psycopg2、numpy、pandas)在编译时默认依赖glibc。你想想看,在Alpine上跑这些库,要么得装一堆编译工具重新编译,要么就得忍受莫名其妙的段错误。
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太简陋了。它不记录依赖的依赖,也不做版本锁定。今天能装,明天可能就装不上。
我个人推荐两个工具:Pipenv和Poetry。它们都解决了依赖锁定和虚拟环境管理的问题。
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社区的标准格式。
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_ENV和LOG_LEVEL:应用级别的配置,运行时可以覆盖
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集成。到时候见。