目錄

用 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-slimDebian 為基礎的精簡版,有 glibc,大多數套件都有預先編譯好的 wheel 可以直接安裝
PYTHONUNBUFFERED=1輸出不經緩衝,docker logs 才能即時看到
PYTHONDONTWRITEBYTECODE=1不產生 .pyc 檔
PIP_NO_CACHE_DIR=1pip 不保留下載快取,映像檔比較小
先 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 自動產生的 Swagger UI API 文件頁面
FastAPI 自動產生的 API 文件(/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 的歡迎頁:

Django 6.1 安裝成功的歡迎頁面
Django 6.1 的預設首頁(DEBUG=True 且尚未設定任何 URL 時才會顯示)

正式環境: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=cacheBuildKit 的快取掛載,uv 下載的套件在多次建置之間共用,但不會留在映像檔中
--lockeduv.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 MBFlask、gunicorn 很小
django-app約 63 MBDjango 本身較大
fastapi-app約 120 MBfastapi[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,必須從原始碼編譯。有兩種做法:

  1. 安裝建置工具(映像檔會變大):

    RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
        && rm -rf /var/lib/apt/lists/*
  2. 用 多階段建置,在第一階段安裝編譯工具並編譯成 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日