Docker 实战教学

前置要求:已阅读基础知识篇,Docker Desktop 已安装并运行。

本文包含三个由浅入深的 Python 实战项目,每个项目都是完整可运行的示例。

  • 完整代码项目地址:gitee

项目一:Flask Web API

目标: 用 Docker 部署一个 Python Web 服务,通过浏览器或接口工具访问它。

学到的技能: 构建镜像、端口映射、查看日志。

1.1 项目文件结构

在你的电脑上新建一个文件夹 flask-demo,创建以下三个文件:

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

1.2 编写代码

app.py

# 项目代码文件中有详细注释
from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/")
def hello():
    return jsonify({"message": "Hello from Docker!", "status": "ok"})

@app.route("/greet/<name>")
def greet(name):
    return jsonify({"message": f"Hello, {name}!"})

if __name__ == "__main__":
    # 监听 0.0.0.0 才能被容器外部访问,不要用 127.0.0.1
    app.run(host="0.0.0.0", port=5000)

requirements.txt

flask==3.1.3

Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]
  • 关于为什么不直接在前面 COPY . . ,而是单独 COPY requirements.txt . ,这是有说法的,与docker的缓存优化有关,详细可以问AI。

1.3 构建并运行

打开终端,进入 flask-demo 目录:

cd flask-demo

# 构建镜像,-t 指定名称
docker build -t flask-demo:1.0 .

构建过程会看到每一步的输出,第一次构建需要下载基础镜像,之后会有缓存,速度很快。

  • 这里提一嘴,docker和git一样有 .dockerignore 文件,可以忽略一些文件,不参与构建镜像。
# 运行容器
# -d 后台运行
# -p 8080:5000 把本机 8080 端口映射到容器内的 5000 端口
# --name flask-app 给容器起名字
docker run -d -p 8080:5000 --name flask-app flask-demo:1.0
# 启动时使用终端
docker run -it -p 8080:5000 --name flask-app flask-demo:1.0 /bin/bash

1.4 验证结果

打开浏览器访问:

  • http://localhost:8080 — 应该看到 {"message": "Hello from Docker!", "status": "ok"}
  • http://localhost:8080/greet/张三 — 应该看到 {"message": "Hello, 张三!"}

查看日志:

docker logs flask-app

1.5 停止和清理

docker stop flask-app
docker rm flask-app

项目二:Python + SQLite 数据存储

目标: 将数据保存到数据库文件,并通过数据卷让数据在容器重启后不丢失。

学到的技能: 数据卷挂载(-v)、数据持久化、环境变量传入。

2.1 项目文件结构

新建文件夹 sqlite-demo

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

同时在 sqlite-demo 旁边创建一个 data 文件夹用于存放数据库文件:

sqlite-demo/
data/          ← 这个文件夹用来持久化数据库

2.2 编写代码

app.py

import sqlite3
import os
from flask import Flask, jsonify, request

app = Flask(__name__)

# 数据库路径,从环境变量读取,默认放在 /data 目录下
DB_PATH = os.environ.get("DB_PATH", "/data/notes.db")

def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = get_db()
    conn.execute(
        "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, content TEXT)"
    )
    conn.commit()
    conn.close()

@app.route("/notes", methods=["GET"])
def list_notes():
    conn = get_db()
    notes = conn.execute("SELECT * FROM notes").fetchall()
    conn.close()
    return jsonify([dict(n) for n in notes])

@app.route("/notes", methods=["POST"])
def add_note():
    content = request.json.get("content", "")
    conn = get_db()
    conn.execute("INSERT INTO notes (content) VALUES (?)", (content,))
    conn.commit()
    conn.close()
    return jsonify({"result": "added", "content": content}), 201

if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000)

requirements.txt

flask==3.0.3

Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

# 创建数据目录
RUN mkdir -p /data

EXPOSE 5000

CMD ["python", "app.py"]

2.3 构建并运行

cd sqlite-demo

docker build -t sqlite-demo:1.0 .

运行时用 -v 挂载本地 data 目录(注意路径要用绝对路径):

# Windows 用户:把 /c/Users/你的用户名/... 替换为实际路径
# 下面的写法适用于 Git Bash / PowerShell

# Git Bash 写法:
docker run -d \
  -p 8080:5000 \
  -v "$(pwd)/../data:/data" \
  --name sqlite-app \
  sqlite-demo:1.0

# PowerShell 写法:
docker run -d -p 8080:5000 -v "${PWD}\..\data:/data" --name sqlite-app sqlite-demo:1.0

2.4 验证数据持久化

添加一条笔记:

用 PowerShell:

Invoke-WebRequest -Uri "http://localhost:8080/notes" `
  -Method POST `
  -ContentType "application/json" `
  -Body '{"content": "我的第一条笔记"}'

或者安装 curl 后:

curl -X POST http://localhost:8080/notes \
  -H "Content-Type: application/json" \
  -d "{\"content\": \"我的第一条笔记\"}"

查看所有笔记:

浏览器访问 http://localhost:8080/notes

测试持久化:删除容器后重新启动,数据还在:

# 删除容器
docker rm -f sqlite-app

# 重新启动(用相同的 -v 参数)
docker run -d -p 8080:5000 -v "${PWD}/../data:/data" --name sqlite-app sqlite-demo:1.0

再访问 http://localhost:8080/notes,之前添加的数据仍然存在。


项目三:Python 数据处理脚本

目标: 用 Docker 运行一个批量处理 CSV 文件的脚本,处理完退出。

学到的技能: 短生命周期容器(处理完就退出)、挂载本地文件目录、--rm 自动清理。

3.1 项目文件结构

新建文件夹 csv-processor

csv-processor/
├── process.py
├── requirements.txt
├── Dockerfile
├── input/
│   └── students.csv    ← 待处理的数据
└── output/             ← 处理结果将输出到这里

3.2 准备测试数据

input/students.csv

name,score,subject
Alice,85,Math
Bob,92,Math
Charlie,78,Math
Alice,90,English
Bob,88,English
Charlie,95,English
Diana,70,Math
Diana,82,English

3.3 编写代码

process.py

import csv
import os
from collections import defaultdict

INPUT_DIR = "/data/input"
OUTPUT_DIR = "/data/output"

def main():
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    # 读取所有 CSV 文件
    all_files = [f for f in os.listdir(INPUT_DIR) if f.endswith(".csv")]
    print(f"找到 {len(all_files)} 个文件:{all_files}")

    for filename in all_files:
        filepath = os.path.join(INPUT_DIR, filename)
        process_file(filepath, filename)

    print("全部处理完成。")

def process_file(filepath, filename):
    scores = defaultdict(list)

    with open(filepath, newline="", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            name = row["name"]
            score = float(row["score"])
            scores[name].append(score)

    # 计算每人的平均分和总分
    results = []
    for name, score_list in scores.items():
        results.append({
            "name": name,
            "count": len(score_list),
            "total": sum(score_list),
            "average": round(sum(score_list) / len(score_list), 2),
        })

    # 按平均分降序排列
    results.sort(key=lambda x: x["average"], reverse=True)

    # 写入结果文件
    output_name = filename.replace(".csv", "_result.csv")
    output_path = os.path.join(OUTPUT_DIR, output_name)

    with open(output_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["name", "count", "total", "average"])
        writer.writeheader()
        writer.writerows(results)

    print(f"[{filename}] 处理完成,结果已保存到 {output_name}")
    for r in results:
        print(f"  {r['name']}: 平均分 {r['average']}")

if __name__ == "__main__":
    main()

requirements.txt

# 本项目只用标准库,无需额外依赖

Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY process.py .

# 数据目录由外部挂载
# 注意,如果RUN时没有指定数据卷,则本地文件系统会创建匿名卷,并且不会随着容器删除而删除
# 可以通过 `docker volume ls` 查看匿名卷,`rm` 删除匿名卷
VOLUME ["/data/input", "/data/output"]

CMD ["python", "process.py"]

3.4 构建并运行

cd csv-processor

docker build -t csv-processor:1.0 .

运行脚本(处理完自动退出,--rm 自动删除容器):

# Git Bash 写法:
docker run --rm \
  -v "$(pwd)/input:/data/input" \
  -v "$(pwd)/output:/data/output" \
  csv-processor:1.0

# PowerShell 写法:
docker run --rm `
  -v "${PWD}/input:/data/input" `
  -v "${PWD}/output:/data/output" `
  csv-processor:1.0

3.5 查看结果

运行后查看 output/ 目录,应该有一个 students_result.csv 文件,内容如下:

name,count,total,average
Charlie,2,173,86.5
Bob,2,180,90.0
Alice,2,175,87.5
Diana,2,152,76.0

(实际排序按平均分降序)


常用调试技巧

查看容器日志

程序出错时第一步:看日志。

docker logs <容器名>
# 看最后 50 行
docker logs --tail 50 <容器名>

进入容器内部排查

docker exec -it <容器名> bash
# 进入后可以用 ls、cat 等命令检查文件是否正确复制进来

检查镜像层

docker history <镜像名>

修改代码后重新部署

Docker 不会自动感知代码变化,每次修改代码后需要:

# 1. 停止并删除旧容器
docker rm -f <容器名>

# 2. 重新构建镜像
docker build -t <镜像名> .

# 3. 重新运行
docker run -d -p 8080:5000 --name <容器名> <镜像名>

综合练习建议

完成三个项目后,尝试以下挑战:

  1. 修改项目一:添加一个 /time 路由,返回当前服务器时间
  2. 扩展项目二:增加删除笔记的接口(DELETE /notes/)
  3. 改造项目三:让脚本接受环境变量来控制排序方式(升序/降序)
    docker run --rm -e SORT_ORDER=asc -v ... csv-processor:1.0
    

下一步学习方向

掌握以上内容后,你已经能够应对大多数单服务的 Docker 部署需求。

继续深入可以学习:

  • Docker Compose:用一个文件管理多个容器(如 Python + MySQL + Redis),是生产环境的标配
  • Docker Hub 推送:将自己构建的镜像上传到仓库,让别人一行命令就能拉取运行
  • 健康检查(HEALTHCHECK):在 Dockerfile 中配置,让 Docker 自动监控容器状态
  • 多阶段构建(Multi-stage Build):减小镜像体积的进阶技巧
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐