目录
用 Docker 部署 Python 应用(Flask、FastAPI、Django)
这一节以 Flask 为主要示例,从项目结构、Dockerfile、构建、运行到开发模式完整走一遍,再说明 FastAPI、Django 和 uv 的差异。所有示例都使用 python:3.14-slim 基础镜像。
Flask:项目结构
flask-app/
├── app.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
└── compose.yaml ← 开发模式才用到
app.py:
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return {"message": "Hello from Flask in Docker"}
requirements.txt:
flask==3.1.*
gunicorn==23.*
版本写成 3.1.* 表示接受 3.1 系列的修正版,但不会自动升到 3.2。正式项目建议用 pip freeze 或 uv(见后面)锁定完整版本。
Flask:Dockerfile
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN useradd --system --no-create-home app
USER app
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "app:app"]
逐行说明:
| 设置 | 作用 |
|---|---|
python:3.14-slim | Debian 为基础的精简版,有 glibc,大多数包都有预先编译好的 wheel 可以直接安装 |
PYTHONUNBUFFERED=1 | 输出不经缓冲,docker logs 才能实时看到 |
PYTHONDONTWRITEBYTECODE=1 | 不生成 .pyc 文件 |
PIP_NO_CACHE_DIR=1 | pip 不保留下载缓存,镜像比较小 |
先 COPY requirements.txt 再 pip install | 只改代码时,安装包那一层可以直接用缓存,参考 构建缓存 |
useradd + USER app | 以非 root 用户运行,参考 Docker 安全性 |
gunicorn ... app:app | 生产环境用 gunicorn,而不是 flask run(开发服务器)。app:app 是「模块 app.py 中的变量 app」 |
--bind 0.0.0.0:8000 | 必须绑定 0.0.0.0,绑 127.0.0.1 的话从容器外连不进来 |
CMD 使用 exec 格式(JSON 数组),gunicorn 是 PID 1,会直接收到 docker stop 送出的 SIGTERM 并优雅地结束,不会像 Dockerfile 命令详解 中的例子等 10 秒才被强制停止。
.dockerignore:
__pycache__
*.pyc
.venv
.env
.git
.venv 一定要排除:本机的虚拟环境是给本机操作系统用的,拷贝进容器不但没用,还会让构建内容变大。
Flask:构建与运行
$ ls -a
.
..
.dockerignore
Dockerfile
app.py
compose.yaml
requirements.txt
$ docker build -t flask-app .
[+] Building 6.0s (11/11) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 364B 0.0s
=> [internal] load metadata for docker.io/library/python:3.14-slim 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 74B 0.0s
=> [1/6] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.8s
=> => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> => extracting sha256:a5883d972f3b60387a858b212a0a24115f143c826e40fdef3270e8474a8f42d6 0.1s
=> => extracting sha256:6aaf5b0aae75c38277901ef64362f50475b879e0ff3ed6610eb2a9f69bcf3295 0.7s
=> => extracting sha256:bb8166eb34e6e8ea17f8c73a7eec8438693d91ebf29eda1d9d3c0dba36b579de 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 907B 0.0s
=> [2/6] WORKDIR /app 0.1s
=> [3/6] COPY requirements.txt . 0.0s
=> [4/6] RUN pip install -r requirements.txt 3.9s
=> [5/6] COPY . . 0.0s
=> [6/6] RUN useradd --system --no-create-home app 0.3s
=> exporting to image 0.7s
=> => exporting layers 0.5s
=> => exporting manifest sha256:7189a55c270e8f9c73535044b6bd76d012e11017d9fb01e26f8da841870a0c0d 0.0s
=> => exporting config sha256:d34e60a2d4bdc7b72f16e2673fd83a3f058e8d73a3a12c5b368a250170caeb43 0.0s
=> => exporting attestation manifest sha256:171f89e1a4fd243b116baac7aa4798fa03b80160a5dad67f360f825635782aa5 0.0s
=> => exporting manifest list sha256:e38ff17f0c938ac71cb8683d8b2527ece5278ac450b1b989173178502d20ee47 0.0s
=> => naming to docker.io/library/flask-app:latest 0.0s
=> => unpacking to docker.io/library/flask-app:latest 0.2s
[4/6] RUN pip install 花了 3.9 秒,是构建中最久的一步。再构建一次,只要 requirements.txt 没改,这一步就会显示 CACHED。
在后台运行并测试:
$ docker run -d --name flask -p 8000:8000 flask-app
4d1e3914573d0ef92e9b11840475100237868cb8aaaddefc8f3765a2b5a7c8ce
$ curl -s http://localhost:8000
{"message":"Hello from Flask in Docker"}
$ docker logs flask
[2026-10-13 00:26:21 +0000] [1] [INFO] Starting gunicorn 23.0.0
[2026-10-13 00:26:21 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2026-10-13 00:26:21 +0000] [1] [INFO] Using worker: sync
[2026-10-13 00:26:21 +0000] [6] [INFO] Booting worker with pid: 6
[2026-10-13 00:26:21 +0000] [7] [INFO] Booting worker with pid: 7
$ docker exec flask whoami
app
curl返回了 JSON,表示服务正常。在浏览器打开http://localhost:8000也会看到同样的内容。docker logs看到 gunicorn 的主进程(pid 1)和两个 worker(--workers 2)。whoami显示app,确认不是以 root 运行。
worker 数量一般设为 CPU 内核数的 2~4 倍左右。gunicorn 也能从环境变量 WEB_CONCURRENCY 读取 worker 数量,但只有在命令中没有写 --workers 时才有效;这个 Dockerfile 的 CMD 已经写了 --workers 2,所以设置 WEB_CONCURRENCY 不会有作用(实测仍然是 2 个 worker)。
开发模式:修改代码自动重新加载
开发时每改一次程序就重新构建镜像太慢。用 Compose 把代码文件夹挂载进容器,并改用 Flask 的开发服务器(--debug 会在文件变动时自动重新加载):
compose.yaml:
services:
web:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
command: ["flask", "--app", "app", "run", "--host", "0.0.0.0", "--port", "8000", "--debug"]
command 会覆盖 Dockerfile 的 CMD,所以同一个 Dockerfile 生产环境跑 gunicorn、开发时跑 Flask 开发服务器。
$ docker compose up -d
[+] Building 0.6s (13/13) FINISHED
=> [internal] load local bake definitions 0.0s
=> => reading from stdin 468B 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 364B 0.0s
=> [internal] load metadata for docker.io/library/python:3.14-slim 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 74B 0.0s
=> [1/6] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 297B 0.0s
=> CACHED [2/6] WORKDIR /app 0.0s
=> CACHED [3/6] COPY requirements.txt . 0.0s
=> CACHED [4/6] RUN pip install -r requirements.txt 0.0s
=> CACHED [5/6] COPY . . 0.0s
=> CACHED [6/6] RUN useradd --system --no-create-home app 0.0s
=> exporting to image 0.3s
=> => exporting layers 0.0s
=> => exporting manifest sha256:b30dc1eb5dd7e3ae2b911558671243f8e2aeb4eb916b88ba0f4a1b6f59612147 0.0s
=> => exporting config sha256:5f7347cc4514246282f5be1a4e1eb3c7cfe90b0ee36c9adeaed934c91bcb2311 0.0s
=> => exporting attestation manifest sha256:2498fb10cf0b98b8be07e54464eaa656528a1fa9324d15544ab7beb474766027 0.0s
=> => exporting manifest list sha256:34a36fcb4d656d04ad0e8f86740d02d314d5c68b67b1ff4fb1b897f053bdb39e 0.0s
=> => naming to docker.io/library/flask-web:latest 0.0s
=> => unpacking to docker.io/library/flask-web:latest 0.2s
=> resolving provenance for metadata file 0.0s
[+] up 3/3
✔ Image flask-web Built 0.7s
✔ Network flask_default Created 0.0s
✔ Container flask-web-1 Started 0.3s
$ curl -s http://localhost:8000
{
"message": "Hello from Flask in Docker"
}
$ docker compose logs web
web-1 | * Serving Flask app 'app'
web-1 | * Debug mode: on
web-1 | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
web-1 | * Running on all addresses (0.0.0.0)
web-1 | * Running on http://127.0.0.1:8000
web-1 | * Running on http://172.19.0.2:8000
web-1 | Press CTRL+C to quit
web-1 | * Restarting with stat
web-1 | * Debugger is active!
web-1 | * Debugger PIN: 127-885-458
web-1 | 172.19.0.1 - - [13/Oct/2026 00:27:50] "GET / HTTP/1.1" 200 -
日志中的 WARNING: This is a development server 是正常的,提醒你这只能用在开发。Debug mode: on 时 Flask 返回的 JSON 会自动排版成多行。
现在用编辑器把 app.py 中的消息改成 Hello again, reloaded! 并存盘,不用重新构建或重启容器,再请求一次:
$ curl -s http://localhost:8000
{
"message": "Hello again, reloaded!"
}
$ docker compose logs --tail 6 web
web-1 | 172.19.0.1 - - [13/Oct/2026 00:26:34] "GET / HTTP/1.1" 200 -
web-1 | * Detected change in '/app/app.py', reloading
web-1 | * Restarting with stat
web-1 | * Debugger is active!
web-1 | * Debugger PIN: 202-909-220
web-1 | 172.19.0.1 - - [13/Oct/2026 00:26:37] "GET / HTTP/1.1" 200 -
日志中的 Detected change in '/app/app.py', reloading 表示 Flask 检测到文件变动并重新加载。这是因为 .:/app 把本机文件夹直接挂载到容器的 /app,容器看到的就是你正在编辑的文件。
开发完用 docker compose down 停止并删除容器。
Windows 与 macOS 注意:Docker Desktop 的文件共享在某些情况下不会传递文件变动通知,如果自动重新加载没有反应,可以把项目放在 WSL 2 的文件系统中(Windows),或改用 Compose 的
develop.watch功能。
FastAPI
FastAPI 使用 ASGI 服务器 uvicorn。requirements.txt:
fastapi[standard]>=0.115
fastapi[standard] 会一起安装 uvicorn 等常用包。main.py:
from fastapi import FastAPI
app = FastAPI(title="Todo API")
@app.get("/")
def index():
return {"message": "Hello from FastAPI"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
Dockerfile 与 Flask 几乎相同,只有最后一行的启动命令不同:
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
构建并运行(-q 只输出镜像 ID):
$ docker build -q -t fastapi-app .
sha256:cd8e9d2a7b6db3a54ecab866c502794cc91f11af78efbcb862eeac962566d2ac
$ docker run -d --name fastapi -p 8000:8000 fastapi-app
b40cf8583a166c85dae4560f6356f201de8eb67c919f3fb709964cd635f0b934
$ curl -s "http://localhost:8000/items/42?q=docker"
{"item_id":42,"q":"docker"}
$ docker exec fastapi pip list 2>/dev/null | grep -iE "^(fastapi|uvicorn|pydantic) "
fastapi 0.143.0
pydantic 2.14.1
uvicorn 0.54.0
/items/42?q=docker 返回 {"item_id":42,"q":"docker"},路径参数 42 被自动转成整数。pip list 可以确认容器内实际安装的版本。
FastAPI 会自动生成 API 文文件,打开 http://localhost:8000/docs:

FastAPI(title="Todo API")开发时在 uvicorn 命令后面加上 --reload,搭配挂载代码文件夹,就和上面的 Flask 开发模式一样。生产环境要多个 worker 时,可以加 --workers 4。
Django
用容器创建 Django 项目
本机不需要安装 Python 或 Django,用容器就能生成项目。先准备 requirements.txt:
django==6.*
gunicorn==23.*
和一个开发用的 Dockerfile:
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
先构建一次(这时还没有 Django 项目),再用这个镜像运行 django-admin startproject,通过挂载把生成的文件写回目前的文件夹;然后重新构建,把项目文件放进镜像:
$ docker build -q -t django-app .
sha256:1ed89110a11971cd5fdec6e565d389a88ffb106c4fe0093274a4923b8d2fc954
$ docker run --rm -v "$(pwd):/app" -w /app django-app django-admin startproject mysite .
$ ls
Dockerfile
manage.py
mysite
requirements.txt
$ docker build -q -t django-app .
sha256:4f6c035057a7507b0a1a6009c6a67506e58c8c90684e4ec7a5df9300e02ff628
$ docker run -d --name django -p 8000:8000 django-app
a4b4df39bd719fb6bee85a6c4f90cc912586b4c80efce9aa43fc1b76522a9c6f
$ docker exec django python -m django --version
6.1.2
$ docker run --rm django-app python manage.py migrate | tail -n 3
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
对应 Windows 的写法(点击标签切换):
docker run --rm -v "$(pwd):/app" -w /app django-app django-admin startproject mysite .
docker run --rm -v "${PWD}:/app" -w /app django-app django-admin startproject mysite .
docker run --rm -v "%cd%:/app" -w /app django-app django-admin startproject mysite .
ls看到多了manage.py和mysite/,是容器内的django-admin写出来的。在 Linux 上这些文件的拥有者会是 root,可以加上--user "$(id -u):$(id -g)"让它们属于你自己。python -m django --version显示6.1.2。migrate用docker run --rm在一个临时容器中运行。这个示例用的是 Django 默认的 SQLite,数据库文件在临时容器中,容器删除后就没了;实际项目请搭配 PostgreSQL,见下方。
打开 http://localhost:8000 看到 Django 的欢迎页:

生产环境:collectstatic 与 gunicorn
生产环境不使用 runserver,而是 gunicorn;静态文件(CSS、JS、admin 的图片)要先用 collectstatic 收集到同一个文件夹,再交给 Nginx 或 WhiteNoise 提供。创建 Dockerfile.prod:
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "mysite.wsgi:application"]
直接构建会失败:
$ docker build -f Dockerfile.prod -t django-prod .
...(省略 Traceback)
1.787 django.core.exceptions.ImproperlyConfigured: You're using the staticfiles app without having set the STATIC_ROOT setting to a filesystem path.
------
Dockerfile.prod:7
--------------------
5 | RUN pip install -r requirements.txt
6 | COPY . .
7 | >>> RUN python manage.py collectstatic --noinput
8 | EXPOSE 8000
9 | CMD ["gunicorn", "--bind", "0.0.0.0:8000", "mysite.wsgi:application"]
--------------------
ERROR: failed to build: failed to solve: process "/bin/sh -c python manage.py collectstatic --noinput" did not complete successfully: exit code: 1
错误消息说得很清楚:startproject 生成的 settings.py 没有设置 STATIC_ROOT,collectstatic 不知道要把文件收集到哪里。在 mysite/settings.py 最后加上:
STATIC_ROOT = BASE_DIR / "staticfiles"
再构建一次就成功了:
$ docker build -f Dockerfile.prod -t django-prod .
[+] Building 3.7s (11/11) FINISHED docker:default
=> [internal] load build definition from Dockerfile.prod 0.0s
=> => transferring dockerfile: 343B 0.0s
=> [internal] load metadata for docker.io/library/python:3.14-slim 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/6] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 3.58kB 0.0s
=> CACHED [2/6] WORKDIR /app 0.0s
=> CACHED [3/6] COPY requirements.txt . 0.0s
=> CACHED [4/6] RUN pip install -r requirements.txt 0.0s
=> [5/6] COPY . . 0.0s
=> [6/6] RUN python manage.py collectstatic --noinput 1.5s
=> exporting to image 2.1s
=> => exporting layers 0.1s
=> => exporting manifest sha256:961f8d60c4ec63f3e3e5c41acbdef0b9df6a967dbd806f9d4d178865a5722dd4 0.0s
=> => exporting config sha256:8a741faf7acfcad1032b73ec67edbe2717396c8db8fe80c969a3b87903cf0c11 0.0s
=> => exporting attestation manifest sha256:651df3cadbdeb850b8ca4ec2c4cd5a30b77e67f1789ba5bb2ab05c42d17fc717 0.0s
=> => exporting manifest list sha256:0e28a34c1ea5ce9838ea3c7204938979ab6a43e7b16d121c059946d6e7a0df01 0.0s
=> => naming to docker.io/library/django-prod:latest 0.0s
=> => unpacking to docker.io/library/django-prod:latest 1.9s
$ docker run -d --name django -p 8000:8000 django-prod
e23efa1aa481b13393faf41f4fb4ff842dee0db8053908846442824dbed97625
$ docker logs django
[2026-10-13 00:28:24 +0000] [1] [INFO] Starting gunicorn 23.0.0
[2026-10-13 00:28:24 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2026-10-13 00:28:24 +0000] [1] [INFO] Using worker: sync
[2026-10-13 00:28:24 +0000] [6] [INFO] Booting worker with pid: 6
$ curl -sI http://localhost:8000/ | head -n 1
HTTP/1.1 200 OK
$ docker exec django ls staticfiles
admin
日志显示由 gunicorn 启动,staticfiles/ 中有收集好的 admin 静态文件。
正式上线前还要调整 settings.py:DEBUG = False、设置 ALLOWED_HOSTS、SECRET_KEY 改从环境变量读取(参考 环境变量)。
数据库迁移
不要把 migrate 写在 CMD 里每次启动都自动运行(多个容器同时启动时可能同时迁移),而是部署时另外运行一次:
docker compose run --rm web python manage.py migrate
搭配 PostgreSQL 的 Compose 设置可以参考 用 Docker 跑 PostgreSQL。
使用 uv 管理包
uv 是速度很快的 Python 包管理工具,用 pyproject.toml 声明依赖,uv.lock 锁定所有包(包含间接依赖)的确切版本。把上面的 Flask 示例改成 uv:
pyproject.toml:
[project]
name = "flask-uv"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"flask>=3.1",
"gunicorn>=23",
]
Dockerfile:
FROM python:3.14-slim
COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /usr/local/bin/uv
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-install-project
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
| 设置 | 作用 |
|---|---|
COPY --from=ghcr.io/astral-sh/uv:0.11 | 直接从 uv 官方镜像拷贝 uv 可执行文件,不用 pip 安装 |
--mount=type=cache | BuildKit 的缓存挂载,uv 下载的包在多次构建之间共用,但不会留在镜像中 |
--locked | uv.lock 和 pyproject.toml 不一致时直接失败,确保安装的版本与锁文件完全相同 |
第一次 sync --no-install-project | 先只安装依赖,这一层可以被缓存;第二次才加入项目本身 |
UV_COMPILE_BYTECODE=1 | 安装时预先编译 .pyc,加快容器启动 |
ENV PATH=... | 让 gunicorn 等命令直接使用 .venv 中的版本 |
uv.lock 要先生成并提交到 Git。本机有 uv 的话运行 uv lock 即可;没有的话可以用容器生成。注意 ghcr.io/astral-sh/uv 镜像里只有 uv 可执行文件,没有 Python,在它里面运行 uv lock 会出现 Failed to discover managed Python installations,所以改用 Python 镜像:
$ docker run --rm -v "$PWD:/app" -w /app python:3.14-slim sh -c "pip install -q --root-user-action=ignore uv && uv lock"
Using CPython 3.14.8 interpreter at: /usr/local/bin/python3
Resolved 9 packages in 96ms
$ ls -a
.
..
.dockerignore
Dockerfile
app.py
pyproject.toml
uv.lock
$ docker build -t flask-uv .
[+] Building 8.3s (14/14) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 497B 0.0s
=> [internal] load metadata for ghcr.io/astral-sh/uv:0.11 0.0s
=> [internal] load metadata for docker.io/library/python:3.14-slim 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 63B 0.0s
=> FROM ghcr.io/astral-sh/uv:0.11@sha256:77280f2f771df71f90786c314fe1bbc1e023feac652969bbf139c280babf2eb7 0.7s
=> => resolve ghcr.io/astral-sh/uv:0.11@sha256:77280f2f771df71f90786c314fe1bbc1e023feac652969bbf139c280babf2eb7 0.0s
=> => extracting sha256:42b502055136e736d5e1d3f5be28fa847276dafc7a55e3fc6bbee14532b41021 0.6s
=> => extracting sha256:95d48f9d48ecf28dfaf61ee94d6692294fd1c4c0527f9d926d6906744b6e3a1d 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 28.88kB 0.0s
=> CACHED [stage-0 1/7] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> [stage-0 2/7] COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /usr/local/bin/uv 0.1s
=> [stage-0 3/7] WORKDIR /app 0.0s
=> [stage-0 4/7] COPY pyproject.toml uv.lock ./ 0.0s
=> [stage-0 5/7] RUN --mount=type=cache,target=/root/.cache/uv uv sync --locked --no-dev --no-install-project 2.3s
=> [stage-0 6/7] COPY . . 0.1s
=> [stage-0 7/7] RUN --mount=type=cache,target=/root/.cache/uv uv sync --locked --no-dev 0.6s
=> exporting to image 4.2s
=> => exporting layers 3.3s
=> => exporting manifest sha256:08d48ef913a933c9b86dfde0768976f21c10d0a39e77e87f14221c2379fa7231 0.0s
=> => exporting config sha256:b7444ccd0f3b14cf614f33f8e0e50b47798a97685e3d8fc8afa400731bde86ae 0.0s
=> => exporting attestation manifest sha256:752398dc502698e4bf24a28a5fbfb7fff9940084c5ae815f7687822c96d0cbc5 0.0s
=> => exporting manifest list sha256:08accafba065f4996215355e6b25a29c024354ea5853bd31cd1eadc0cb15efb7 0.0s
=> => naming to docker.io/library/flask-uv:latest 0.0s
=> => unpacking to docker.io/library/flask-uv:latest 0.8s
$ docker run -d --name flaskuv -p 8000:8000 flask-uv
6a6fcaf04c1005467f010e1016981c1309fc59f6ed363c29dda8c4f5bd657eed
$ curl -s http://localhost:8000
{"message":"Hello from Flask in Docker"}
$ docker exec flaskuv which gunicorn
/app/.venv/bin/gunicorn
$ docker images flask-uv
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
flask-uv:latest 08accafba065 266MB 67.8MB U
对应 Windows 的写法(点击标签切换):
docker run --rm -v "$PWD:/app" -w /app python:3.14-slim sh -c "pip install -q --root-user-action=ignore uv && uv lock"
docker run --rm -v "${PWD}:/app" -w /app python:3.14-slim sh -c "pip install -q --root-user-action=ignore uv && uv lock"
docker run --rm -v "%cd%:/app" -w /app python:3.14-slim sh -c "pip install -q --root-user-action=ignore uv && uv lock"
which gunicorn 显示的是 /app/.venv/bin/gunicorn,表示使用的是 uv 创建的虚拟环境。
镜像大小比较
$ docker images --filter reference=flask-app --filter reference=fastapi-app --filter reference=django-app --filter reference=python:3.14-slim
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
django-app:latest 4f6c035057a7 243MB 52.7MB
fastapi-app:latest cd8e9d2a7b6d 300MB 68.9MB
flask-app:latest e38ff17f0c93 189MB 45.8MB
python:3.14-slim a2b82f3c4855 180MB 45.7MB
| 镜像 | 比基础镜像多出 | 主要原因 |
|---|---|---|
flask-app | 约 9 MB | Flask、gunicorn 很小 |
django-app | 约 63 MB | Django 本身较大 |
fastapi-app | 约 120 MB | fastapi[standard] 包含 uvicorn、pydantic-core、httptools、uvloop 等多个包 |
用 uv 构建的 flask-uv 为 266 MB,比 flask-app 大,原因是多了 uv 可执行文件本身,以及 UV_COMPILE_BYTECODE 生成的 .pyc。如果很在意大小,可以用 多阶段构建:在第一阶段用 uv 安装,第二阶段只拷贝 .venv,最终镜像就不包含 uv。
需要编译的包
slim 镜像没有编译器。如果 pip install 出现 gcc: not found 或 error: command 'gcc' failed 之类的错误,表示某个包没有适合的预先编译 wheel,必须从原代码编译。有两种做法:
-
安装构建工具(镜像会变大):
RUN apt-get update && apt-get install -y --no-install-recommends build-essential \ && rm -rf /var/lib/apt/lists/* -
用 多阶段构建,在第一阶段安装编译工具并编译成 wheel,第二阶段只安装 wheel,最终镜像不包含编译器。
大多数热门包(numpy、pandas、psycopg 等)都提供 Linux 的预先编译 wheel,在 slim 上可以直接安装。alpine 使用 musl libc,需要的是另一种 wheel(musllinux),部分包没有提供时就得自行编译,构建时间和失败几率都比较高,这也是 Python 项目通常建议用 slim 而非 alpine 的原因。
重点回顾
- 生产环境用 gunicorn(WSGI)或 uvicorn(ASGI),不要用开发服务器。
- 先拷贝 requirements.txt 再安装,善用构建缓存。
- 开发时挂载代码文件夹,让框架自动重新加载。
- Django 的 collectstatic 需要先设置
STATIC_ROOT。
动手练习
练习 1:把 Flask 示例的 gunicorn worker 数量改成 4,而不修改 Dockerfile。
答案
在镜像名称后面写上完整的命令,覆盖 CMD:
docker run -d -p 8000:8000 flask-app gunicorn --bind 0.0.0.0:8000 --workers 4 app:app
用 docker logs 确认出现 4 行 Booting worker。只设置 -e WEB_CONCURRENCY=4 没有效果,因为 CMD 中已经写了 --workers 2。
练习 2:FastAPI 示例的 API 文文件在哪个网址?
答案
http://localhost:8000/docs,由 FastAPI 自动生成。
常见问题
Python 镜像要选 slim 还是 alpine?
建议 slim。alpine 使用 musl libc,部分包没有预先编译的 wheel,需要自行编译,构建慢又容易失败。
print 的输出在 docker logs 中延迟出现?
设置 PYTHONUNBUFFERED=1,让输出不经缓冲。
要用 pip 还是 uv?
两者都可以。uv 速度快,并用 uv.lock 锁定所有包版本;本章两种写法都有实测。
更新于 2026年10月14日