第 85 课 - 容器化入门:Docker 与 Python
学习目标
通过本课学习,你将能够:
- 理解容器化的核心价值:明白"在我机器上可以运行"问题为何存在,以及 Docker 如何解决它。
- 掌握 Dockerfile 的编写:学会为 Python 应用编写清晰、规范的 Dockerfile。
- 独立构建并运行 Docker 容器:完成从构建镜像到启动容器的完整流程。
- 实现数据持久化与网络配置:了解如何在容器中挂载数据卷和管理容器网络。
- 初步接触多容器编排:了解使用
docker-compose管理多个相关联容器的基本方法。
核心概念
什么是容器化?
想象一下,你开发了一个很棒的 Python Web 应用,它在你的电脑上运行得非常完美。但当你的同事尝试在它的电脑上运行时,却报错了,因为操作系统版本、Python 环境、第三方库版本等条件不一致。这就是经典的 "在我机器上可以运行" 问题。
容器化 就是解决这个问题的方案。它像是一个 "标准集装箱"。你将你的应用程序代码、运行时环境(如 Python 解释器)、系统工具和库、以及所有依赖配置,都打包到一个名为 容器(Container) 的轻量级、独立的单元中。这个容器可以在任何安装了 Docker 的机器上(你的笔记本、同事的台式机、公司的服务器、云端)以完全相同的方式启动和运行,保证了环境的绝对一致。
Docker 是实现容器化最流行的工具。
Docker 的核心组件
- 镜像(Image):一个只读的模板,相当于一个"食谱"或"蓝图"。它包含了运行应用所需的一切。例如,你可以有一个基于 Ubuntu 的 Python 3.9 系统的镜像,里面预装了 Flask 库。
- 容器(Container):镜像的运行实例。你可以从一个镜像启动多个容器,就像用同一个食谱(镜像)做出多道菜(容器)。容器是动态的,你可以启动、停止、删除它们。
- Dockerfile:一个文本文件,包含了一系列指令,用于告诉 Docker 如何自动构建(Build)一个镜像。它就是你编写"食谱"的地方。
- Docker Hub:一个类似 GitHub 的仓库,用于存储和分发公共(或私有)的 Docker 镜像。你可以拉取(pull)别人做好的镜像,也可以推送(push)自己的镜像上去。
一个生动的比喻
- Dockerfile 是你写的 "菜谱"。
- 镜像(Image) 是根据这个菜谱做好的、封装好的 "半成品料理包"。
- 容器(Container) 是把料理包 "加热" 后得到的 "一盘热菜",你可以同时做好几盘相同的菜。
- Docker Hub 是 "美食平台",你可以在上面找到别人分享的菜谱(Dockerfile)或直接购买成品料理包(镜像)。
代码示例
让我们通过一个完整的例子,将一个简单的 Python Flask 应用容器化。
第一步:创建一个简单的 Flask 应用
创建一个新目录 my_flask_app,并在其中创建文件 app.py。
# app.py
from flask import Flask
import os
app = Flask(__name__)
# 从环境变量获取消息,默认为 "Hello from Dockerized Python!"
MESSAGE = os.getenv('GREETING_MESSAGE', 'Hello from Dockerized Python!')
@app.route('/')
def home():
return f'''
<h1>{MESSAGE}</h1>
<p>This app is running inside a Docker container!</p>
<p>Python version: {os.sys.version}</p>
'''
if __name__ == '__main__':
# 监听所有可用网络接口,端口 5000
app.run(host='0.0.0.0', port=5000)
在同一目录下,创建一个 requirements.txt 文件,声明依赖:
Flask==2.3.2
第二步:编写 Dockerfile
在 my_flask_app 目录下,创建名为 Dockerfile 的文件(注意,没有文件扩展名)。
# Dockerfile
# 1. 使用官方 Python 3.9 的精简版镜像作为基础
FROM python:3.9-slim
# 2. 设置工作目录为 /app
WORKDIR /app
# 3. 先将依赖文件复制进去(利用 Docker 缓存层优化)
COPY requirements.txt .
# 4. 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 5. 将当前目录下所有文件复制到容器的 /app 目录
COPY . .
# 6. 声明容器运行时监听的端口
EXPOSE 5000
# 7. 定义容器启动时执行的命令
CMD ["python", "app.py"]
指令解释:
FROM:指定基础镜像。WORKDIR:为后续指令设置工作目录。COPY:从构建上下文(你的本地目录)复制文件到镜像中。RUN:在镜像构建过程中执行命令(如安装软件)。EXPOSE:声明容器打算使用的端口(仅为文档作用,实际映射需在运行时指定)。CMD:指定容器启动时默认执行的命令。
第三步:构建 Docker 镜像
在 my_flask_app 目录下,打开终端,执行以下命令:
# -t 给镜像起一个名字(标签),后面跟着"点"表示使用当前目录作为构建上下文
docker build -t my-flask-app:1.0 .
构建成功后,你会看到 Successfully tagged my-flask-app:1.0 的提示。
第四步:运行 Docker 容器
# -p 用于端口映射,将主机的 8080 端口映射到容器的 5000 端口
# -d 表示以后台模式运行容器(守护态)
docker run -p 8080:5000 -d my-flask-app:1.0
现在,打开你的浏览器,访问 http://localhost:8080,你应该能看到 Flask 应用的欢迎页面。
扩展:数据持久化与多容器示例
如果你的应用需要读写文件(如数据库、日志),容器停止后数据会丢失。可以使用 数据卷(Volume) 来持久化数据。
# 运行一个容器,并将主机的 `./data` 目录挂载到容器的 `/app/data` 目录
docker run -p 8080:5000 -v ./data:/app/data -d my-flask-app:1.0
对于更复杂的应用(如 Web + 数据库),可以使用 docker-compose.yml 文件来定义和运行多容器应用。
创建一个 docker-compose.yml 文件:
version: '3.8'
services:
web:
build: . # 基于当前目录的 Dockerfile 构建
ports:
- "8080:5000"
depends_on:
- db # 表示 web 服务依赖 db 服务
environment:
- DATABASE_URL=postgresql://user:password@db:5432/mydb
db:
image: postgres:13 # 直接使用官方的 PostgreSQL 镜像
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data # 使用命名卷持久化数据库数据
volumes:
postgres_data: # 声明一个命名卷
然后使用一条命令启动整个应用栈:
docker-compose up -d
实践练习
练习 1:基础容器化
- 创建一个简单的 Python 脚本
hello.py,功能是打印 "Hello from the Container!" 和当前时间。 - 为它编写一个 Dockerfile。
- 构建镜像并运行一个容器,在终端中看到输出。
预期输出:
Hello from the Container!
Current time: 2023-10-27 14:30:00.123456
(时间会随当前系统时间变化)
练习 2:带环境变量的应用
- 修改上面的
app.py,增加一个/greet/<name>路由,问候语从环境变量GREETING读取(默认为Hello)。 - 重新构建镜像。
- 运行容器时,通过
-e参数传入环境变量GREETING=Welcome,验证效果。
预期输出(访问 http://localhost:8080/greet/Alice):
Welcome, Alice!
练习 3:多容器编排挑战(选做)
- 基于上面的
docker-compose.yml示例,尝试将你的 Flask 应用与一个 Redis 缓存服务连接。 - 修改 Flask 应用,使其能够连接到 Redis 并记录访问次数。
- 使用
docker-compose启动并测试整个应用。
常见错误
- Dockerfile 指令顺序错误:例如,在安装依赖(
RUN pip install ...)之前就复制了所有代码(COPY . .)。这会导致每次修改代码后都要重新安装依赖,降低构建速度。应该先复制依赖文件,安装依赖,再复制代码。 - 端口映射混淆:
docker run -p 8080:5000中,左边(8080)是主机端口,右边(5000)是容器内部端口。如果容器应用监听的是 8000 端口,却映射到主机的 8080,那么访问localhost:8080是连不上的。 - 忽略
.dockerignore文件:构建上下文(docker build .中的.)会包含当前目录所有文件,如.git、__pycache__、node_modules等。这会极大增加构建时间和镜像体积。应创建一个.dockerignore文件来排除它们。# .dockerignore .git __pycache__ *.pyc .env venv - 构建上下文过大:上一个问题的延伸。确保你的项目目录干净,或者使用
.dockerignore,否则会感觉构建过程异常缓慢。 - 不理解镜像层缓存:Docker 构建是分层的。如果修改了代码(
COPY . .这一层),它之后的所有层(如RUN指令)都会重新执行。合理安排指令顺序(把不常变的层放在前面)可以极大利用缓存,加速重复构建。
小结
本课我们踏入了容器化的世界,核心收获如下:
- Docker 解决了环境一致性问题,通过将应用及其所有依赖打包到一个独立的容器中,实现了"构建一次,到处运行"。
- 核心流程是:编写 Dockerfile -> 构建镜像 -> 运行容器。
- 一个良好的 Dockerfile 应该层次清晰,善用
.dockerignore和构建缓存。 - 使用
-p进行端口映射,-v进行数据卷挂载以实现持久化。 - 对于多服务应用,
docker-compose是定义和运行复杂环境的利器。
掌握了 Docker,你就拥有了将 Python 应用标准化、可移植地部署到任何环境的强大能力。下一课,我们将学习如何将这个过程自动化,进入 CI/CD 的领域。