☰
Python容器化实战:用Docker与Compose实现应用部署
2026/10/7 3:12:29 网站建设 项目流程

1. 为什么想用Docker跑Python应用

先说个真实的场景。我之前维护过一个内部工具,用Flask写了个数据看板,本地跑得好好的,但换到同事电脑上就各种报错。有的是Python版本不对,有的缺了某个系统依赖库,还有的连MySQL连接驱动都没装全。三天两头有人来找我,“你这个项目怎么跑不起来”。后来我把整个应用塞进Docker镜像,一条命令就能在任意机器上启动,再也没人因为环境问题来找我了。

这个痛点,只要是搞Python开发的人基本都遇到过。Python本身跨平台,但Python应用不跨平台——你依赖的那些包、系统库、Python版本、环境变量,任何一个对不上都跑不起来。Docker解决的就是这个问题:把应用连同它的完整运行环境一起打包,镜像到哪儿,环境就到哪儿。

容器化带来的好处不只是“换个机器能跑”这么简单。拿我实际体验来说,最直观的三点:一是开发环境和生产环境可以做到完全一致,再也不会出现“我这儿没问题啊”这种话;二是依赖隔离做得干净,一个项目一个容器,Python版本、依赖库互不干扰,不用再折腾虚拟环境那一套;三是部署成本大幅降低,以前上线要准备服务器、装环境、配依赖,现在服务器上只要装好Docker,剩下的就是一个docker run命令的事。

这篇文章适合谁看?我觉得只要是写过Python、又不想在环境配置上反复折腾的人,都可以花几分钟读一遍。如果你已经装了Docker,可以直接跳到第三节看实际案例;如果你是零基础,那建议从头看完,每一步我都按实际操作顺序写的,照着敲就行。

2. 动手之前:环境准备与核心概念

2.1 Windows、Mac、Linux下的Docker安装

先说Linux。大多数服务器都是Linux环境,安装Docker最简单,官方提供了自动脚本,一条命令搞定:

curl -fsSL https://get.docker.com | bash

装完启动服务,顺手设置开机自启:

systemctl enable --now docker

然后验证一下:

docker --version docker compose version

Windows和Mac用户就直接装Docker Desktop,这是官方出品的桌面版,自带图形界面,装完就能用。Windows注意一点:Docker Desktop依赖WSL2(Windows Subsystem for Linux),如果你之前没启用过,安装过程会提示你启用。装完之后可以在设置里把资源限制调一下,默认配置在内存紧张的老机器上可能会卡。

有一个常见的坑,Windows启动Docker Desktop时报错,提示“virtualization support not detected”或者“failed to start because virtualisation support wasn't detected”,这基本就是没有开启BIOS里的硬件虚拟化。解决办法是重启电脑,进BIOS设置,把Intel VT-x或者AMD SVM打开,然后重启再启动Docker Desktop就正常了。

2.2 镜像、容器、仓库,这三个概念别搞混

我见过不少初学者把镜像和容器混为一谈。打个比方:镜像是“类”,容器是“实例”。镜像是静态的文件快照,是一个模板;容器是镜像运行起来之后的进程,里面有状态、有数据、能交互。你从仓库拉取镜像,然后用这个镜像创建容器,容器可以启动、停止、删除,但镜像不会受影响。

镜像仓库就是存放镜像的地方。Docker Hub是官方默认仓库,国内访问有时候速度很慢,可以用镜像加速器,在Docker Desktop的设置里找到Docker Engine,把registry-mirrors配置填上就能加速拉取。这一段比较关键,不然你后面拉镜像可能要等很久。启动docker后,先用docker info看一下版本和配置,确认一切正常再继续。

3. 写一个规范的Dockerfile,把Python应用打包成镜像

3.1 从零开始:最简单的Dockerfile

我先拿一个最普通的Flask应用举例。项目目录长这样:

flask-demo/ ├── app.py ├── requirements.txt └── Dockerfile

app.py内容很简单:

from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello from Dockerized Python!"

对应的Dockerfile:

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 5000 CMD ["python", "app.py"]

每个指令我都说一下为什么这么写:

FROM python:3.11-slim,指定基础镜像,slim版本比完整版小很多,只保留运行Python所需的最小依赖,生产环境用slim就够了。完整版里带了一堆编译工具链和文档,体积大不说,还增加了攻击面。

WORKDIR /app,设置容器内的工作目录,后续命令默认在这个目录下执行。这行不是必须的,但强烈建议加上,不然你后面要反复写绝对路径。

COPY requirements.txt .和RUN pip install为什么要分开写?这里面有个缓存优化的门道。Docker构建镜像时会一层层缓存,如果这两行合在一起写,后续只要依赖文件有一行变动,整个pip install都得重新跑一遍,每次构建都要等几分钟。分开写,只有requirements.txt真正变了,才会重新执行pip install。

-i https://pypi.tuna.tsinghua.edu.cn/simple是pip镜像源参数,国内服务器用官方源装依赖经常超时,换成清华源基本秒下。

最后用CMD而不是RUN来启动应用,RUN是构建时执行,CMD是运行时执行。这是两回事,写反了镜像根本起不来。

3.2 生产级Dockerfile的五个进阶细节

刚才那个Dockerfile能跑,但离“生产可用”还有差距。我实际部署过几个项目后,总结了几个提升点:

第一,使用非root用户运行应用。默认容器是以root身份运行的,一旦应用有漏洞被攻击者利用了,对方直接拿到root权限。创建一个普通用户来跑应用:

FROM python:3.11-slim RUN useradd -m appuser WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . USER appuser EXPOSE 5000 CMD ["python", "app.py"]

第二,设置时区。容器默认是UTC时区,你打印日志、记录时间的时候会发现比北京时间少8个小时。在Dockerfile里设置一下:

ENV TZ=Asia/Shanghai

第三,处理好__pycache__缓存目录。你本地项目里的缓存文件在容器里没用,还会让镜像变大。在项目根目录建一个.dockerignore,内容和.gitignore类似:

__pycache__/ *.py[cod] .git/ .venv/

第四,注意构建上下文。COPY . .会把整个项目目录(包括dockerignore之外的所有文件)都发送给Docker守护进程。如果你的项目里有大数据文件或者模型文件,构建会非常慢。所以.dockerignore要好好写。

第五,如果项目里需要用到GPU、特殊的系统库,或者体积可以接受,可以考虑多阶段构建。比如先在一个带编译器的镜像里编译依赖,再把编译产物复制到slim镜像里:

FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --prefix=/install --no-cache-dir -r requirements.txt FROM python:3.11-slim WORKDIR /app COPY --from=builder /install /usr/local COPY . .

这样最终镜像里只有运行需要的产物,没有中间编译工具链,镜像体积能小不少。

构建镜像的命令是:

docker build -t flask-demo:latest .

-t指定镜像名称和标签,末尾的.指构建上下文目录,别漏了。

4. 用docker compose编排完整的Python + MySQL应用

4.1 为什么单容器不够用

上面那个Flask应用是单容器的,跑起来很简单。但现实中绝大多数的Python应用都要依赖数据库、Redis、消息队列这些外部服务。如果每个服务都手动docker run,不仅命令多、容易出错,还要自己管理网络,非常麻烦。

docker compose就是干这个事的。它用YAML文件描述整个应用栈需要哪些容器、怎么连接、数据怎么存,一条docker compose up命令全部搞定。

还有一个典型场景:开发环境要装MySQL。直接在主机上装MySQL,要处理安装包、初始化密码、配置远程连接,麻烦不说,出了问题还把系统搞乱了。用Docker拉一个MySQL镜像,10秒钟就有一个干净的MySQL实例,用完docker compose down直接销毁,主机上没有残留。

4.2 编排一个Flask + MySQL 8.0的完整示例

我直接给你一个能跑的配置。项目结构:

flask-mysql-demo/ ├── app.py ├── requirements.txt ├── Dockerfile └── docker-compose.yml

app.py增加数据库连接:

import os import pymysql from flask import Flask, jsonify app = Flask(__name__) def get_conn(): return pymysql.connect( host=os.environ.get("DB_HOST", "localhost"), port=int(os.environ.get("DB_PORT", 3306)), user=os.environ.get("DB_USER", "root"), password=os.environ.get("DB_PASSWORD", "123456"), database=os.environ.get("DB_NAME", "demo"), charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor ) @app.route("/") def hello(): return "Hello from Dockerized Python!" @app.route("/health") def health(): try: conn = get_conn() conn.ping() conn.close() return jsonify({"status": "ok"}) except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)

注意这里数据库连接的主机名是通过环境变量DB_HOST传入的,这是容器化部署的关键:容器之间互相通信不能用localhost,要用服务名。在compose里,服务名就是主机名。

docker-compose.yml:

services: mysql: image: mysql:8.0 container_name: flask_mysql environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: demo ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 web: build: . container_name: flask_web environment: DB_HOST: mysql DB_PORT: 3306 DB_USER: root DB_PASSWORD: 123456 DB_NAME: demo ports: - "5000:5000" depends_on: mysql: condition: service_healthy volumes: mysql_data:

这里有几个点值得细说。

ports映射格式是宿主机端口:容器端口。3306映射到宿主机,意味着你本机的MySQL客户端也可以连这个容器里的MySQL。但注意,如果你的宿主机已经装了MySQL占了3306端口,会有冲突。这种情况可以把映射改成3307:3306,访问的时候用3307端口即可。

MySQL的healthcheck是关键。如果直接让web服务启动就跑,MySQL可能还没准备好,web服务连接数据库就会报错。加了健康检查之后,要等MySQL真正就绪,depends_on配合condition: service_healthy,web服务才会启动。我踩过这个坑,一开始没加健康检查,Flask应用启动时连不上数据库,整个容器反复重启。

启动整个应用栈:

docker compose up -d

-d表示后台运行。查看状态:

docker compose ps

查看日志:

docker compose logs -f web

停止并删除容器:

docker compose down

注意docker compose down默认不会删除数据卷,MySQL的数据还会保留在mysql_data卷里。如果你要彻底清空数据,加上-v参数:

docker compose down -v

启动docker后访问http://localhost:5000/health,出现{"status": "ok"}说明整个链路已经打通。

4.3 数据持久化与容器间的通信机制

容器是临时性的,删了就没了,但数据库数据不能跟着没。这就是volumes存在的意义。

在compose配置里,mysql_data:/var/lib/mysql这一行,把命名卷挂载到容器内MySQL的数据目录。数据写在宿主机上,哪怕容器删了重建,数据还在。这是生产环境必须做的。

命名卷和绑定挂载是两种方式。命名卷由Docker管理,位置在Docker的数据目录里,docker volume inspect可以查看具体路径。绑定挂载则是直接把宿主机的某个目录挂进去,比如./data:/var/lib/mysql。开发阶段用绑定挂载方便,可以直接看到文件;生产环境我推荐用命名卷,由Docker统一管理,迁移也更方便。

容器之间的通信,要理解compose默认会创建一个网络,所有在这个compose文件里的服务都在这个网络中。服务之间通过服务名互相访问,比如web服务里连接mysql,host填mysql就可以了。如果用localhost,那指的是web容器自己,里面没有MySQL,连接必然失败。

要在宿主机访问容器内的MySQL客户端,可以直接进入容器操作:

docker exec -it flask_mysql mysql -uroot -p123456

exec命令是在运行中的容器里执行命令,-it表示交互式分配终端。

5. 常见问题与排查技巧实录

5.1 端口冲突与容器命名冲突

端口冲突是最常见的问题。启动时报错port is already allocated,说明宿主机的这个端口已经被占用了。用下面的命令查一下:

netstat -ano | grep 3306

看到占用进程后,要么换宿主机端口,要么停掉占用端口的进程。容器命名冲突同理,报错container name already exists,用docker ps -a看看是不是有一个退出状态的容器还占着名字。把旧的删掉再重新启动:

docker rm flask_web

5.2 时区错误导致日志时间不对

容器默认UTC时间,日志里的时间戳比北京时间少8小时。解决方式前面说过了,在Dockerfile里加:

ENV TZ=Asia/Shanghai

或者在compose的environment里加:

services: web: environment: TZ: Asia/Shanghai

有的基础镜像还需要安装tzdata包时区才会生效。如果设置后仍然不对,在Dockerfile里加上:

RUN apt-get update && apt-get install -y tzdata

5.3 pip安装依赖超时或失败

国内网络环境下从官方PyPI拉依赖经常超时。解决方法是换镜像源,pip参数-i指定:

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

在Dockerfile里也一样,加上-i参数。如果使用的是pip配置,也可以设置环境变量:

ENV PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple

5.4 镜像构建慢

构建慢通常有三个原因:依赖没有利用缓存、基础镜像过大、构建上下文太大。依赖缓存问题前面讲过了,把requirements.txt单独COPY出来先安装,利用Docker的层缓存可以大幅加速重复构建。基础镜像方面,能用slim就用slim,需要编译的依赖可以用多阶段构建。构建上下文方面,把.dockerignore写全,别把node_modules、大数据文件这些东西一股脑发进构建进程。

构建时看到[Warning]提示“secret in build args”之类的,要注意别把密码硬编码到Dockerfile里。敏感信息应该通过运行时环境变量传入,或者用Docker的secrets机制管理。这是我后来养成的一个习惯:镜像里永远不带生产密码。

5.5 容器起一下就退出,怎么看日志

容器启动后立刻退出,最常见的错误是启动命令有问题,比如路径不对、依赖缺失。先把启动方式改成前台模式,同时打印日志:

docker run -it -p 5000:5000 flask-demo:latest

-it把容器变成交互模式,日志直接输出到终端,这样可以第一眼看到报错。另外一个排查入口是查看已退出的容器日志:

docker logs flask_web

docker logs命令即使容器已经退出,只要容器没有删除,日志都还在。这个命令是你排障的第一工具,先看日志再猜原因。

访问容器内的MySQL,确认数据是否正常:

docker exec -it flask_mysql bash

进去之后再用MySQL客户端查询,这样能区分是网络问题还是数据库本身的问题。

6. 写在最后:几个亲测有效的习惯

用Docker跑Python应用到现在有一段时间了,有几条经验算是踩坑换来的。

第一,凡是涉及环境变量的配置,统一从compose文件的environment传入,不要写死在代码里。换环境的时候只改compose文件,应用代码一行都不用动。

第二,MySQL这类有状态的服务,数据卷一定要挂载。不挂载的话,容器一删数据全没。我一开始在测试环境偷懒没挂,后来一次误操作把容器删了,库里积累的数据全部丢失,从备份恢复折腾了半天。

第三,镜像标签永远不要用latest。构建的时候指定明确的版本号,比如myapp:20240128,这样回滚的时候能精确定位到旧版本镜像,不用猜latest到底是哪天构建的。

第四,生产环境的Dockerfile,务必使用非root用户运行应用,这不只是规范,是安全底线。容器里默认root运行的话,一旦应用被攻破,攻击者对整个容器乃至宿主机都有完全的控制权。创建专用用户只需要两行配置,值得养成习惯。

最后,调试容器内网络问题的时候,可以用docker exec进入容器,装一个curl或者ping,先测试容器间是否能互通,再测试应用层是否正常。排查的思路就是从底层往上排查:端口是否被监听、进程是否存活、依赖是否缺失、应用日志报了什么错。掌握了这个链路,大多数问题都能在几分钟内定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询