目次

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 のダウンロードキャッシュを保持しないため、イメージが小さくなります
requirements.txt を先に COPY してから pip installコードだけを変更した場合、パッケージのインストールレイヤーでキャッシュをそのまま利用できます。ビルドキャッシュを参照してください
useradd + USER approot 以外のユーザーで実行します。Docker のセキュリティを参照してください
gunicorn ... app:app本番環境では(開発サーバーの)flask run ではなく gunicorn を使います。app:app は「モジュール app.py 内の変数 app」を意味します
--bind 0.0.0.0:80000.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 は必ず除外してください。ローカルの仮想環境はローカルの OS 用です。コンテナにコピーしても役に立たず、ビルドの内容が増えるだけです。

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)と2つのワーカー(--workers 2)が表示されます。
  • whoami の結果は app で、root として実行されていないことを確認できます。

ワーカー数は通常、CPUコア数の2~4倍程度に設定します。gunicorn は環境変数 WEB_CONCURRENCY からワーカー数を読み取ることもできますが、コマンドに --workers を指定していない場合に限り有効です。この Dockerfile の CMD にはすでに --workers 2 が指定されているため、WEB_CONCURRENCY を設定しても反映されません(実際に試してもワーカー数は2のままです)。

開発モード:コードを変更すると自動で再読み込み

開発中にコードを変更するたびにイメージを再ビルドするのは時間がかかります。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.11uv の公式イメージから uv 実行ファイルを直接コピーします。pip でインストールする必要はありません
--mount=type=cacheBuildKit のキャッシュマウントです。uv がダウンロードしたパッケージを複数回のビルドで共有しますが、イメージには残りません
--lockeduv.lock と pyproject.toml が一致しない場合は失敗し、インストールするバージョンがロックファイルと完全に一致することを保証します
最初の sync --no-install-projectまず依存関係だけをインストールします。このレイヤーはキャッシュできます。2 回目にプロジェクト自体を追加します
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 が追加されるためです。サイズを重視する場合は、マルチステージビルドを使います。第 1 ステージで uv を使ってインストールし、第 2 ステージでは .venv だけをコピーすれば、最終イメージに uv は含まれません。

コンパイルが必要なパッケージ

slim イメージにはコンパイラがありません。pip install で gcc: not found や error: command 'gcc' failed のようなエラーが出た場合、該当するパッケージに適合するビルド済み wheel がなく、ソースコードからコンパイルする必要があります。方法は 2 つあります。

  1. ビルドツールをインストールします(イメージのサイズは大きくなります)。

    RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
        && rm -rf /var/lib/apt/lists/*
  2. マルチステージビルドを使います。第 1 ステージでコンパイルツールをインストールして wheel を作成し、第 2 ステージでは wheel だけをインストールすれば、最終イメージにコンパイラは含まれません。

人気のあるパッケージの多く(numpy、pandas、psycopg など)は Linux 用のビルド済み wheel を提供しているため、slim ではそのままインストールできます。alpine は musl libc を使うため、別の種類の wheel(musllinux)が必要です。一部のパッケージでは提供されておらず、自分でコンパイルする必要があります。そのため、ビルド時間が長くなり、失敗する可能性も高くなります。これが、Python プロジェクトでは通常 alpine より slim が推奨される理由です。

まとめ

  • 本番環境では gunicorn(WSGI)または uvicorn(ASGI)を使い、開発用サーバーは使わないでください。
  • まず requirements.txt をコピーしてからインストールし、ビルドキャッシュを活用します。
  • 開発時はコードのディレクトリをマウントすると、フレームワークが自動的に再読み込みします。
  • Django の collectstatic を使うには、先に STATIC_ROOT を設定する必要があります。

練習問題

練習 1:Dockerfile を変更せずに、Flask の例で gunicorn の worker 数を 4 にしてください。

解答

イメージ名の後にコマンド全体を指定して、CMD を上書きします。

docker run -d -p 8000:8000 flask-app gunicorn --bind 0.0.0.0:8000 --workers 4 app:app

docker logs を使い、Booting worker が 4 行表示されることを確認します。CMD に --workers 2 がすでに指定されているため、-e WEB_CONCURRENCY=4 だけを設定しても効果はありません。

練習 2:FastAPI の例で API ドキュメントを確認できる URL はどれですか?

解答

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日