
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 versionWindows和Mac用户就直接装Docker Desktop这是官方出品的桌面版自带图形界面装完就能用。Windows注意一点Docker Desktop依赖WSL2Windows Subsystem for Linux如果你之前没启用过安装过程会提示你启用。装完之后可以在设置里把资源限制调一下默认配置在内存紧张的老机器上可能会卡。有一个常见的坑Windows启动Docker Desktop时报错提示“virtualization support not detected”或者“failed to start because virtualisation support wasnt 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 └── Dockerfileapp.py内容很简单from flask import Flask app Flask(__name__) app.route(/) def hello(): return Hello from Dockerized Python!对应的DockerfileFROM 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 TZAsia/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 --frombuilder /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.ymlapp.py增加数据库连接import os import pymysql from flask import Flask, jsonify app Flask(__name__) def get_conn(): return pymysql.connect( hostos.environ.get(DB_HOST, localhost), portint(os.environ.get(DB_PORT, 3306)), useros.environ.get(DB_USER, root), passwordos.environ.get(DB_PASSWORD, 123456), databaseos.environ.get(DB_NAME, demo), charsetutf8mb4, cursorclasspymysql.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(host0.0.0.0, port5000)注意这里数据库连接的主机名是通过环境变量DB_HOST传入的这是容器化部署的关键容器之间互相通信不能用localhost要用服务名。在compose里服务名就是主机名。docker-compose.ymlservices: 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_healthyweb服务才会启动。我踩过这个坑一开始没加健康检查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服务里连接mysqlhost填mysql就可以了。如果用localhost那指的是web容器自己里面没有MySQL连接必然失败。要在宿主机访问容器内的MySQL客户端可以直接进入容器操作docker exec -it flask_mysql mysql -uroot -p123456exec命令是在运行中的容器里执行命令-it表示交互式分配终端。5. 常见问题与排查技巧实录5.1 端口冲突与容器命名冲突端口冲突是最常见的问题。启动时报错port is already allocated说明宿主机的这个端口已经被占用了。用下面的命令查一下netstat -ano | grep 3306看到占用进程后要么换宿主机端口要么停掉占用端口的进程。容器命名冲突同理报错container name already exists用docker ps -a看看是不是有一个退出状态的容器还占着名字。把旧的删掉再重新启动docker rm flask_web5.2 时区错误导致日志时间不对容器默认UTC时间日志里的时间戳比北京时间少8小时。解决方式前面说过了在Dockerfile里加ENV TZAsia/Shanghai或者在compose的environment里加services: web: environment: TZ: Asia/Shanghai有的基础镜像还需要安装tzdata包时区才会生效。如果设置后仍然不对在Dockerfile里加上RUN apt-get update apt-get install -y tzdata5.3 pip安装依赖超时或失败国内网络环境下从官方PyPI拉依赖经常超时。解决方法是换镜像源pip参数-i指定pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple在Dockerfile里也一样加上-i参数。如果使用的是pip配置也可以设置环境变量ENV PIP_INDEX_URLhttps://pypi.tuna.tsinghua.edu.cn/simple5.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_webdocker 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先测试容器间是否能互通再测试应用层是否正常。排查的思路就是从底层往上排查端口是否被监听、进程是否存活、依赖是否缺失、应用日志报了什么错。掌握了这个链路大多数问题都能在几分钟内定位。