零基础入门python69:为 FastAPI 项目构建可复现 Docker 镜像

零基础入门python69:为 FastAPI 项目构建可复现 Docker 镜像

上一篇课后练习讲解

FastAPI 项目最终验收包括注册、权限、缓存和 AI 降级。只有测试通过的代码才进入镜像,镜像构建成功也不能替代健康检查。

上一篇课后练习完整答案

上一篇练习已经落实到完整文件,运行下面代码可以观察本篇要求的成功和失败状态;数据库写入全部放在明确事务边界内。

答案要点:smoke 脚本串联 health/docs 和主要业务状态码;最终验收同时保存 Docker、数据库、Redis、MongoDB 与日志证据。

完整答案文件:scripts/smoke.py

完整参考答案文件

完整文件:scripts/smoke.py

python 复制代码
import requests
base = "http://127.0.0.1:8000"
assert requests.get(base + "/health").status_code == 200
assert requests.get(base + "/docs").status_code == 200
print("smoke ok")

验收:运行项目测试(FastAPI/Flask 使用 python -m pytest -q,Django 使用 python manage.py test),再按本文 curl 或 Docker 命令检查预期状态码。

完整参考答案文件

下面是交付项目中真实存在的完整文件 fastapi-blog/tests/test_api.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码;复制单个函数会丢失上下文,因此这里提供整份文件。

python 复制代码
from app.ai import get_ai_client
from app.main import app

def test_health(client): assert client.get('/api/health').json() == {'status':'ok'}
def test_register_login(client, token): assert token

def test_article_flow(client, headers):
    created = client.post('/api/articles', headers=headers, json={'title':'第一篇','content':'这是一段足够长的文章正文'}); assert created.status_code == 201
    article_id = created.json()['id']
    assert client.get('/api/articles?q=第一').json()['total'] == 1
    assert client.post(f'/api/articles/{article_id}/comments', headers=headers, json={'content':'写得很好'}).status_code == 201
    assert client.post(f'/api/articles/{article_id}/likes', headers=headers).status_code == 201
    assert client.post(f'/api/articles/{article_id}/likes', headers=headers).status_code == 409
    assert client.delete(f'/api/articles/{article_id}', headers=headers).status_code == 204

def test_other_user_cannot_edit(client, headers):
    article = client.post('/api/articles', headers=headers, json={'title':'权限测试','content':'这是一段足够长的文章正文'}).json()
    client.post('/api/auth/register', json={'email':'bob@example.com','password':'password123'})
    token = client.post('/api/auth/login', data={'username':'bob@example.com','password':'password123'}).json()['access_token']
    response = client.patch(f"/api/articles/{article['id']}", headers={'Authorization':f'Bearer {token}'}, json={'title':'恶意修改','content':'这是一段足够长的文章正文'})
    assert response.status_code == 403

def test_ai_dependency_can_be_replaced(client, headers):
    class FakeAI:
        def suggest(self, content): return {'title':'测试标题','summary':'测试摘要','tags':['test']}
    app.dependency_overrides[get_ai_client] = lambda: FakeAI()
    response = client.post('/api/ai/writing-assistant', headers=headers, json={'content':'这是一段用于测试AI写作助手的足够长内容'})
    assert response.json()['title'] == '测试标题'
    app.dependency_overrides.pop(get_ai_client, None)

def test_upload_and_stream(client):
    uploaded = client.post('/api/tools/avatar', files={'file': ('avatar.png', b'fake-png', 'image/png')})
    assert uploaded.status_code == 200
    assert uploaded.json()['filename'].endswith('.png')
    rejected = client.post('/api/tools/avatar', files={'file': ('note.txt', b'text', 'text/plain')})
    assert rejected.status_code == 415
    stream = client.get('/api/tools/stream')
    assert 'event: done' in stream.text

一、Dockerfile

dockerfile 复制代码
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
COPY alembic.ini .
USER nobody
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

二、构建和运行

powershell 复制代码
docker build -t fastapi-blog:dev .
docker run --rm -p 8000:8000 --env-file .env fastapi-blog:dev

容器失败先看 docker logs,再检查环境变量、监听地址和数据库 DNS。API 必须监听 0.0.0.0,容器内的 localhost 只代表自己。

验收与课后练习

镜像启动后 /health 返回 200;镜像层不包含密钥;容器停止后没有未提交事务。下一篇用 Compose 编排 API、PostgreSQL 和 Redis。

三、逐层理解 Dockerfile

dockerfile 复制代码
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN python -m pip install --no-cache-dir -r requirements.txt
COPY app ./app
COPY alembic.ini .
RUN addgroup --system app && adduser --system --ingroup app app
USER app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

先复制依赖文件是为了命中构建缓存;PYTHONDONTWRITEBYTECODE 减少镜像垃圾文件,PYTHONUNBUFFERED 让日志立即输出;非 root 用户降低文件写入和进程逃逸风险。EXPOSE 只是文档,不会自动映射宿主端口,运行时仍要 -p 8000:8000。不要把 .envblog.db、测试上传文件复制进镜像,.dockerignore 至少包含 .venv__pycache__.env*.dbtests.git

四、构建证据和常见错误

powershell 复制代码
docker build --pull=false -t fastapi-blog:0.1 .
docker image inspect fastapi-blog:0.1 --format '{{.Config.User}}'
docker run --rm -p 8000:8000 --env-file .env fastapi-blog:0.1
curl http://127.0.0.1:8000/health

预期 inspect 输出 app,健康检查返回 200。ModuleNotFoundError 表示 COPY 路径或工作目录错误;容器启动后外部访问失败通常是 Uvicorn 仍监听 127.0.0.1permission denied 说明上传目录没有给运行用户写权限,应在镜像中创建 /app/uploads 并设置属主,而不是改回 root。

五、本篇练习

为镜像增加只读根文件系统实验,明确哪些目录需要可写;给 docker run 增加 --read-only --tmpfs /tmp,记录上传和日志行为。下一篇把镜像、PostgreSQL、Redis 编排为一套可复现 Compose。

六、镜像体积和安全检查

使用 docker history fastapi-blog:0.1 查看每一层,确认没有把 .env 或密钥写入 RUN echo 层;用 docker scout cves fastapi-blog:0.1(环境支持时)检查基础镜像漏洞。依赖升级应在 lock 文件变更中完成并重新跑测试,不能只更新 python:3.11-slim 标签后直接上线。镜像构建成功只是"文件能打包",还要验证容器在非 root、无写权限和无外网条件下仍能启动健康检查。

本篇结束:完整模块文件

本节不是代码片段,而是本篇结束时该模块的完整版本。请先备份旧文件,再整体替换;替换后重新运行本篇命令和测试。阅读时重点看本篇新增的函数、事务边界和错误处理,未涉及的代码先不要自行删减。

完整配置

text 复制代码
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","8000"]
相关推荐
<花开花落>1 小时前
Python 项目迁移到 uv:经验小结与可复用工作流
python·uv
databook1 小时前
正态分布撒谎时:用柯西分布捕捉生活中的“黑天鹅”
python·数据挖掘·数据分析
Java后端的Ai之路1 小时前
LangChain Deep Agents 从入门到企业实战
开发语言·人工智能·python·langchain·deepagents
2601_962381581 小时前
Mac和Windows,哪种电脑适合新手学Python|数智码力分享
windows·python·mac·编程环境·学习入门
会飞的拖把1 小时前
Python文件操作详解:从文件读写到os、shutil模块实战
开发语言·python
RS迷途小书童1 小时前
Python 解析大疆无人机 SRT 字幕日志
开发语言·python·无人机
我不会起名字3221 小时前
一天一道算法题(29):单调栈
java·数据结构·python·算法·leetcode·golang·单调栈
萧鼎1 小时前
2026新库实测:sbxloop 1.5.24 让 AI Agent 在 Docker 沙箱中安全自治,告别环境混乱
人工智能·python·开源·开发工具·ai agent
栀椩2 小时前
CODrone 无人机航拍车辆检测
pytorch·python·yolo