docker

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > 云和虚拟化 > docker > Docker部署Python

Docker部署Python应用的问题与优化实践

作者:Number272

作为一个经常用Docker部署Python应用的开发者,大家肯定遇到过改了一行代码,重新打包镜像,然后眼睁睁看着Docker重新下载几百MB的依赖包,本文就来和大家讲讲如何优化这一问题吧

作为一个经常用Docker部署Python应用的开发者,我想大家都遇到过这样的情况:改了一行代码,重新打包镜像,然后眼睁睁看着Docker重新下载几百MB的依赖包...

最开始我以为这是正常现象,直到有一次改了个小bug要紧急发布,结果光重新构建镜像就花了10分钟,我彻底受不了了。

这篇文章记录了我解决这个问题的全过程,希望能帮到有同样困扰的朋友。

问题背景分析

原来的痛点

在最初的Dockerfile中,我通常会这样写:

FROM python:3.10-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

这种写法看起来没什么问题,但实际使用中问题很大:每次改代码重新构建,Docker都要重新下载所有Python包。为什么会这样?答案就在Docker的缓存机制里。

Docker缓存机制揭秘

Docker构建镜像的时候,其实就像是在搭积木。每一条指令(RUNCOPY等)都会产生一个新的镜像层,Docker会给每一层计算一个哈希值。

Docker是个很"懒"的家伙:如果发现某一层的哈希值和之前构建的完全一样,它就直接复用之前的结果,这就是缓存。

但是有个坑:一旦某一层发生变化,后面所有的层都会重新构建

举个例子,看看这个"有问题"的Dockerfile:

FROM python:3.10-slim
WORKDIR /app
COPY . .                    # 第3层:复制所有文件
RUN pip install -r requirements.txt  # 第4层:安装依赖
CMD ["python", "app.py"]    # 第5层:启动命令

这样写的话,只要你改了一行代码,第3层的哈希就变了。Docker一看:"哎呀,第3层变了,那第4、5层也不能信任了,全部重来!"

于是pip install又开始漫长的下载过程...

正确的做法是这样的:

FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .     # 第3层:只复制依赖文件
RUN pip install -r requirements.txt  # 第4层:安装依赖
COPY . .                    # 第5层:复制其他文件
CMD ["python", "app.py"]    # 第6层:启动命令

这样一来,只要requirements.txt没变,Docker就会说:"第3、4层我之前构建过,直接用缓存!"只有第5、6层需要重新构建。

为什么这样就快了?

这就是为什么调整顺序能带来巨大性能提升的原因!

多阶段构建优化方案

基于上面的缓存原理,我设计了一个多阶段构建的解决方案:

# 多阶段构建 - 构建阶段
FROM python:3.10-slim AS builder

# 设置工作目录
WORKDIR /app

# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# 安装构建依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# 先复制并安装依赖,利用Docker缓存机制
COPY requirements.txt .

# 创建虚拟环境并安装依赖
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir -r requirements.txt

# 最终镜像
FROM python:3.10-slim
ENV OMP_NUM_THREADS=1
ENV KMP_INIT_AT_FORK=FALSE

# 设置工作目录
WORKDIR /app

# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONPATH=/app \
    PORT=8080

# 安装运行时依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgl1-mesa-glx \
    libglib2.0-0 \
    libsm6 \
    libxext6 \
    libxrender-dev \
    curl \
    && rm -rf /var/lib/apt/lists/*

# 从构建阶段复制虚拟环境
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# 复制应用代码
COPY app ./app

# 创建数据目录
RUN mkdir -p /app/data/uploads \
    /app/data/results \
    /app/data/queue

# 初始化数据库
RUN python -m app.init_db

# 暴露端口 (Cloud Run 使用 8080)
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:${PORT}/health || exit 1

# 启动命令
CMD exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT} --workers 1

关键优化点解析

1. 多阶段构建

这样做的好处是:

2. 虚拟环境的使用

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

虚拟环境提供了更好的依赖隔离,也便于在多阶段构建中传递。

自动化部署脚本

为了简化部署流程,我还编写了一个自动化脚本:

#!/bin/bash

# 设置错误时退出
set -e

# 日志函数
log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
}

# 错误处理函数
handle_error() {
    log "错误: $1"
    exit 1
}

# 检查 Docker 是否运行
if ! docker info > /dev/null 2>&1; then
    handle_error "Docker 未运行,请先启动 Docker"
fi

log "开始清理旧容器和镜像..."

# 停止并删除旧容器(如果存在)
if docker ps -a | grep -q myapi; then
    log "停止并删除旧容器 myapi..."
    docker stop myapi || log "停止容器失败,可能已经停止"
    docker rm myapi || log "删除容器失败,可能已经删除"
fi

# 删除旧镜像(如果存在)
if docker images | grep -q myapi; then
    log "删除旧镜像 myapi..."
    docker rmi myapi || log "删除镜像失败,可能已经删除"
fi

log "开始构建新镜像..."
# 构建新镜像
if ! docker build -t myapi .; then
    handle_error "镜像构建失败"
fi

log "启动新容器..."
# 运行新容器
if ! docker run -d --name myapi -p 127.0.0.1:8080:8080 myapi; then
    handle_error "容器启动失败"
fi

# 等待容器启动
log "等待容器启动..."
sleep 5

# 检查容器是否正在运行
if ! docker ps | grep -q myapi; then
    handle_error "容器启动失败,请检查日志"
fi

log "部署完成!"
log "容器状态:"
docker ps | grep myapi

# 显示容器日志
log "容器日志:"
docker logs myapi

这个脚本的优点:

效果对比

优化前

优化后

最佳实践建议

通过这次优化实践,我总结了几个关键点:

补充一个.dockerignore示例:

__pycache__/
*.pyc
*.pyo
*.pyd
*.db
*.sqlite3
.env
.git
.gitignore
.dockerignore
tests/
.pytest_cache/

CI/CD场景提示: 如果在GitHub Actions等CI环境中构建,可以使用--cache-from参数进一步加速构建过程。

实际应用案例

我把这套优化方案应用到了自己的项目部署中,效果确实明显。现在每次代码更新的部署时间从原来的几分钟缩短到了不到一分钟,开发体验提升了不少。

写在最后

折腾了这么久,总算是解决了Docker重复下载依赖的问题。现在每次更新代码,几十秒就能完成部署,再也不用喝着咖啡等构建了

3句话总结全文:

其实很多时候问题的解决方案并不复杂,关键是要静下心来分析问题的本质。Docker的缓存机制本来就是为了解决这类问题而设计的,我们只需要合理利用就行。

到此这篇关于Docker部署Python应用的问题与优化实践的文章就介绍到这了,更多相关Docker部署Python内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文