Docker 環境と Python FastAPI で最小 API を実装する：MongoDB

# Docker 環境と Python FastAPI で最小 API を実装する：MongoDB

こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 24.04 で Python FastAPI Web アプリケーションを作成し、最小限の REST API を実装する方法を紹介します。
![fastapi_and_mongodb_on_docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e126b01c-acf4-d95f-549d-27be5aa2fc85.png)

## 目的

Windows 11 の Linux でクラウド開発します。

こちらから記事の一覧がご覧いただけます。

https://qiita.com/studio_meowtoon/items/6119ab05d8b13527efd8

## 実現すること

ローカル環境の Ubuntu の Docker 環境で、Dockerfile からビルドした Python FastAPI サービスのカスタムコンテナーと MongoDB データベースコンテナーを起動します。

**Python ファイルのアプリをコンテナーとして起動**
```mermaid
flowchart LR
    terminal --> uvicorn
    subgraph Ubuntu
        subgraph Docker
            subgraph app-container[api-todo-fastapi]
                subgraph python
                    uvicorn <--> app[main.py]
                end
            end
            subgraph db-container[mongodb-todo]
                subgraph mongodb
                    db_todo
                end
            end
            app[main.py] <--> db_todo
        end
    end
style Ubuntu fill:#FFBCA4, stroke:#E0460F
style Docker fill:#EEFDFF, stroke:#74B9DC
style python fill:#DCFFDC, stroke:#2F872F
style app-container fill:#FFD7E4, stroke:#E13C73
style db-container fill:#FFD7E4, stroke:#E13C73
style app fill:#ECECFF, stroke:#9674DC
style uvicorn fill:#FFFFDE, stroke:#B4AE43
style mongodb fill:#FFFFDE, stroke:#B4AE43
```

**実行環境**

|要素|概要|
|:--|:--|
|terminal|ターミナル|
|Ubuntu|OS|
|Docker|コンテナー実行環境|

**API サービス コンテナー**

|要素|概要|
|:--|:--|
|api-todo-fastapi|API サービス カスタムコンテナー|
|python|Python 実行環境|
|uvicorn|Web サーバー|
|main.py|Python スクリプト|

**データベース コンテナー**

|要素|概要|
|:--|:--|
|mongodb-todo|データベースコンテナー|
|mongodb|DB サーバー|
|db_todo|データベース|

## 技術トピック

### FastAPI とは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### [FastAPI (ファストエーピーアイ)](https://fastapi.tiangolo.com/ja/tutorial/)

:::note info
FastAPI は、Python の Web フレームワークの中でもとくに注目されている新しいフレームワークであり、多くの特徴とメリットがあります。

|キーワード|内容|
|:--|:--|
|高速な性能|FastAPI は非常に高速なWebフレームワークであり、ASGI (Asynchronous Server Gateway Interface) を活用することで高いパフォーマンスを提供します。非同期処理をサポートすることで、同時リクエストの処理が可能となります。|
|シンプルで直感的な記述|FastAPI はシンプルで直感的な構文を持ち、少ないコードで強力な API を構築できます。Python の型ヒント (Type Hints) を活用して、入力の検証やドキュメントの自動生成が行えます。|
|自動生成されたドキュメント|FastAPI は Swagger UI や Redoc を使って API のドキュメントを自動生成します。API エンドポイント、リクエストボディ、レスポンスなどが自動的にドキュメント化され、API の理解やテストが容易になります。|
|入力検証とシリアライゼーション|FastAPI はリクエストの入力データを自動的に検証し、Pydantic を使って型情報をシリアライズします。入力データのバリデーションエラーを自動的に処理できます。|
|依存性注入|FastAPI は依存性注入をサポートしており、依存関係の注入によってコードの再利用性とテスト容易性が向上します。依存性注入を活用することで、アプリケーションの部品を疎結合に保ちながら効果的に結合できます。|
|サポートされるデータベースと ORM|FastAPI はさまざまなデータベースと ORM (Object-Relational Mapping) をサポートしています。たとえば、SQLAlchemy、Tortoise ORM、MongoDB、Redis などが利用可能です。|
|セキュリティ|FastAPI は標準的なセキュリティ機能を提供しています。OAuth2.0 や API キーのサポート、HTTPS 対応など、セキュアな API の構築が可能です。|
:::

</details>

## 開発環境

+ Windows 11 Home 23H2 を使用しています。

:::note info
[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作しますので macOS の方も参考にして頂けます。  
:::

<details>

WSL (Microsoft Store アプリ版) [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/cd0c4a607d451dbf5430)

```sh
> wsl --version
WSL バージョン: 2.2.4.0
カーネル バージョン: 5.15.153.1-2
WSLg バージョン: 1.0.61
```

Ubuntu [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/cd0c4a607d451dbf5430)

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04 LTS
Release:        24.04
Codename:       noble
```

Docker [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/3bceee25ac67cc73ebbe)

```sh
$ docker --version
Docker version 27.0.3, build 7d4bcd8
```

</details>

:::note warn
この記事では基本的に **Ubuntu のターミナルで操作**を行います。**Vim を使用してコピペする方法**をはじめて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
:::

https://qiita.com/studio_meowtoon/items/7771ad48676926e41463

## 作成する REST API の仕様

|エンドポイント|HTTPメソッド|説明|リクエストBody|レスポンスBody|
|:--|:--|:--|:--|:--|
|/todos         |GET   |すべての ToDo アイテムを取得します。|None         |ToDo アイテムの配列|
|/todos/complete|GET   |完了した ToDo アイテムを取得します。|None         |ToDo アイテムの配列|
|/todos/{id}    |GET   |ID で ToDo アイテムを取得します。   |None         |ToDo アイテム|
|/todos         |POST  |新しい ToDo アイテムを追加します。  |ToDo アイテム|ToDo アイテム|
|/todos/{id}    |PUT   |既存の ToDo アイテムを更新します。  |ToDo アイテム|None|
|/todos/{id}    |DELETE|ID で ToDo アイテムを削除します。   |None         |None|

## データベース コンテナーの起動

こちらの記事で、ToDo アプリ用の NoSQL データベースを作成し、Docker コンテナーとして起動する手順をご確認いただけます。

https://qiita.com/studio_meowtoon/items/ac170257fac6c772c7e2

データベース コンテナーが起動していることを確認します。

```sh
$ docker ps
CONTAINER ID   IMAGE        COMMAND                   CREATED          STATUS          PORTS                                           NAMES
6b411c62bc95   mongo-base   "docker-entrypoint.s…"   41 minutes ago   Up 41 minutes   0.0.0.0:27017->27017/tcp, :::27017->27017/tcp   mongodb-todo
```

:::note alert
コンテナー間通信するために **net-todo** という Docker ネットワークをあらかじめ作成しています。ご注意ください。
:::

## REST API を実装する手順

### プロジェクトの作成

プロジェクトフォルダーを作成します。  
※ ~/tmp/restapi-fastapi をプロジェクトフォルダーとします。

```sh
$ mkdir -p ~/tmp/restapi-fastapi
$ cd ~/tmp/restapi-fastapi
```

### model ファイルの作成

:::note alert
このシリーズの記事で、**RDBMS** と **NoSQL** の両方で**同じ API 操作**を行います。そのため、ID フィールドは数値型ではなく文字列として定義しています。統一性を保ちつつ異なるデータストアで動作することを目指しています。ご了承ください。
:::

**model.py** ファイルを作成します。

```sh
$ mkdir -p app
$ vim app/model.py
```

ファイルの内容

```py:app/model.py
from __future__ import annotations
from os import environ
from typing import Optional
from datetime import datetime
from bson import ObjectId
from pydantic import BaseModel
from motor.core import AgnosticClient, AgnosticDatabase, AgnosticCollection
from motor.motor_asyncio import AsyncIOMotorClient

# データベース接続情報を環境変数から取得
CONNECTION_STRING = (
    f"mongodb://"
    f"{environ.get('DB_USER')}:"
    f"{environ.get('DB_PASSWORD')}@"
    f"{environ.get('DB_HOST')}:"
    f"{environ.get('DB_PORT')}/?authSource=admin"
)

# データベース接続・コレクション取得
client: AgnosticClient = AsyncIOMotorClient(CONNECTION_STRING)
database: AgnosticDatabase = client[environ.get('DB_NAME')] # データベース名
collection: AgnosticCollection = database["todos"] # コレクション名

# ToDo エンティティを表すクラス
class Todo(BaseModel):
    id: Optional[str] = None
    content: Optional[str]
    created_date: Optional[datetime] = None
    completed_date: Optional[datetime] = None

    # MongoDB ドキュメントID を id に変換した Todo インスタンスを生成します。
    @classmethod
    def from_mongo(cls, data: dict) -> Todo:
        data["id"] = str(data.pop("_id"))
        return cls(**data)

    # Todo インスタンスの id を MongoDB ドキュメントIDに変換します。
    def to_mongo(self) -> dict[str, str]:
        data = self.model_dump()
        data["_id"] = ObjectId(data.pop("id"))
        return data

    # 新しい Todo インスタンスの作成に必要なフィールドを設定します。
    def set_create_fields(self, create_dict: dict) -> None:
        create_dict["_id"] = ObjectId()
        create_dict["created_date"] = datetime.utcnow()

    # 更新されるフィールドを差分として設定します。
    def map_update_fields(self, update_dict: dict, exist_dict: dict) -> None:
        update_dict.pop("id")
        update_dict["_id"] = exist_dict["_id"]
        update_dict["created_date"] = exist_dict["created_date"]
```

<details>
<summary>説明を開きます。</summary>

**ライブラリのインポートと型定義**

|要素|説明|
|:--|:--|
|annotations|\_\_future\_\_ モジュールを使用して、型アノテーションの評価を遅延し、循環参照の問題を回避しています。このようにすることで、型アノテーションを含むクラスを定義する際にクラス名を文字列として使用できます。|
|environ|環境変数を扱うためのモジュールです。|
|Optional|型アノテーションで使用され、フィールドがオプション (存在しないこともある) であることを示します。|
|datetime|日時を表現するためのモジュールです。|
|ObjectId|MongoDB のオブジェクト ID を表現するためのデータ型です。|
|BaseModel|Pydantic の基本モデルクラスで、データのバリデーションや変換を行うために使用されます。|
|AgnosticClient|Motor の非同期クライアントのインターフェイスです。このクライアントを使用して MongoDB サーバーとの接続を確立し、非同期の操作を実行できます。|
|AgnosticDatabase|Motor の非同期データベースのインターフェイスです。このデータベースを使用して MongoDB のデータベースに対する操作を実行できます。|
|AgnosticCollection|Motor の非同期コレクションのインターフェイスです。このコレクションを使用して MongoDB のコレクション (テーブルに相当) に対する操作を実行できます。|
|AsyncIOMotorClient|Motor ライブラリのクライアントクラスの実装の1つで、非同期コードと統合された非同期版の MongoDB クライアントです。FastAPI アプリケーション内で非同期の MongoDB 操作を実行するために使用されます。|

**ToDo クラスの定義**

|要素|説明|
|:--|:--|
|Todo|Pydantic の BaseModel を継承しています。|
|id、content、created_date、completed_date|フィールドとして id、content、created_date、completed_date を定義しています。|
|from_mongo|MongoDB のオブジェクト ID から Todo インスタンスを生成するためのクラスメソッドです。のオブジェクト ID から取り出した _id を id フィールドに変換してインスタンスを生成します。|
|to_mongo|Todo インスタンスを MongoDB のオブジェクト ID に変換するためのメソッドです。id フィールドを _id に変換し、のオブジェクト ID を生成します。|
|set_create_fields, map_update_fields|Todo インスタンスの作成や更新に関するものです。新しいインスタンスの作成には _id や created_date フィールドを設定します。|

</details>

### main ファイルの作成

**main.py** ファイルを作成します。

```sh
$ mkdir -p app
$ vim app/main.py
```

ファイルの内容

```py:app/main.py
from typing import List
from bson import ObjectId
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.model import collection, Todo

# app オブジェクト
app = FastAPI()

# CORS 設定：適切に修正してください。
app.add_middleware(
    CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]
)

# すべての ToDo アイテムを取得します。
@app.get("/todos")
async def get_all_todos() -> List[Todo]:
    todos = []
    cursor = collection.find({})
    async for document in cursor:
        todos.append(Todo.from_mongo(document))
    return todos

# 完了した ToDo アイテムを取得します。
@app.get("/todos/complete")
async def get_completed_todos() -> List[Todo]:
    todos = []
    cursor = collection.find({"completed_date": {"$ne": None}})
    async for document in cursor:
        todos.append(Todo.from_mongo(document))
    return todos

# ID で ToDo アイテムを取得します。
@app.get("/todos/{id}")
async def get_todo(id: str) -> Todo:
    document = await collection.find_one({"_id": ObjectId(id)})
    todo = Todo.from_mongo(document)
    return todo

# 新しい ToDo アイテムを追加します。
@app.post("/todos")
async def create_todo(create: Todo) -> Todo:
    create_dict = create.model_dump()
    create.set_create_fields(create_dict)
    new_todo = Todo.from_mongo(create_dict)
    await collection.insert_one(new_todo.to_mongo())
    return new_todo

# 既存の ToDo アイテムを更新します。
@app.put("/todos/{id}")
async def update_todo(id: str, update: Todo) -> None:
    exist_dict = await collection.find_one({"_id": ObjectId(id)})
    update_dict = update.model_dump()
    update.map_update_fields(update_dict, exist_dict)
    await collection.update_one({"_id": ObjectId(id)}, {"$set": update_dict})
    return None

# ID で ToDo アイテムを削除します。
@app.delete("/todos/{id}")
async def delete_todo(id: str) -> None:
    await collection.delete_one({"_id": ObjectId(id)})
    return None
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|FastAPI アプリケーションの設定|FastAPI アプリケーションが作成されています。FastAPI は Python の Web フレームワークで、高速で直感的な API を構築できます。|
|CORS ミドルウェアの設定|CORS ミドルウェアが追加されています。これにより、異なるドメインからのリクエストも許可されるようになります。|
|ToDo アイテムの取得|/todos エンドポイントでは、MongoDB のコレクションからすべての ToDo アイテムを非同期で取得しています。|
|完了した ToDo アイテムの取得|/todos/complete エンドポイントでは、完了日付が設定されている ToDo アイテムを非同期で取得しています。|
|ToDo アイテムの取得 (ID指定)|/todos/{id} エンドポイントでは、特定の ToDo アイテムを ID で取得しています。MongoDB の find_one メソッドを使用しています。|
|ToDo アイテムの作成|/todos エンドポイントへの POST リクエストで新しい ToDo アイテムを作成しています。入力データは Pydantic モデルでバリデーションされ、MongoDB に保存されます。|
|ToDo アイテムの更新|/todos/{id} エンドポイントへの PUT リクエストで既存の ToDo アイテムを更新しています。変更があるフィールドのみを更新し、MongoDB の update_one メソッドを使用しています。|
|ToDo アイテムの削除|/todos/{id} エンドポイントへの DELETE リクエストで特定の ToDo アイテムを削除しています。MongoDB の delete_one メソッドを使用しています。|

</details>

pipx をインストールします。

```sh
$ sudo apt update
$ sudo apt install pipx
```

バージョンを確認します。

```sh
$ pipx --version
1.4.3
```

pipx ツールへの PATH を追加します。

```sh
$ pipx ensurepath
```

パッケージを仮想環境にインストールします。

```sh
$ pipx install uvicorn
```

仮想環境にパッケージを追加でインストールします。

```sh
$ pipx inject uvicorn fastapi pydantic motor
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|pipx install|pipx コマンドは Python のパッケージマネージャーで、パッケージのインストールや管理を仮想環境を通じて行います。|
|uvicorn|Uvicorn という ASGI サーバーをインストールします。Uvicorn は FastAPI アプリケーションを実行するためのサーバーです。|
|fastapi|FastAPI という Python の Web フレームワークをインストールします。FastAPI は高速で直感的な Web API を作成できる特徴があります。|
|pydantic|Pydantic というライブラリをインストールします。Pydantic はデータ検証と設定のためのライブラリで、FastAPI で入力データのバリデーションに使用されます。|
|motor|Motor という非同期 MongoDB ドライバーをインストールします。Motor は非同期操作をサポートする MongoDB との連携に使用されます。|

</details>

### アプリのビルドと起動

環境変数を作成します。

```sh
export DB_HOST=localhost
export DB_PORT=27017
export DB_NAME=db_todo
export DB_USER=root
export DB_PASSWORD=password
```

:::note alert
この環境変数が**一時的なもの**であることに注意してください。
:::

アプリを起動します。
※ アプリを停止するときは ctrl + C を押します。

```sh
$ uvicorn app.main:app \
    --host 0.0.0.0 --port 5000
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|uvicorn|ASGI (Asynchronous Server Gateway Interface) アプリケーションを実行するためのサーバーです。FastAPI アプリケーションは ASGI アプリケーションであり、このコマンドを使用してサーバーを起動します。|
|app.main:app|FastAPI アプリケーションを指定しています。app.main は app モジュール内の main オブジェクト (FastAPIアプリケーション) を指します。app の後の :app は、起動する FastAPI アプリケーションを指定しています。|
|--host 0.0.0.0|サーバーがリッスンするホストアドレスを指定します。0.0.0.0 は、すべてのネットワークインターフェイスからの接続を受け入れることを意味します。|
|--port 5000|サーバーがリッスンするポート番号を指定します。ここではポート番号 5000 を指定しています。|

</details>

:::note info
ここまでの手順で、**Ubuntu** でアプリを起動することができました。
:::

### アプリの動作確認

別ターミナルから curl コマンドで確認します。

:::note warn
必要な場合、jq をインストールします。

```sh
$ sudo apt update
$ sudo apt install jq
```

:::

#### GET: /todos エンドポイントの動作確認

すべての ToDo アイテムを取得します。

```sh
$ curl -s http://localhost:5000/todos | jq '.'
```

レスポンス

```sh
[
// 省略
  {
    "id": "66b7f3866c2fb3b3a7149f4a",
    "content": "運動する",
    "created_date": "2024-08-10T23:11:02.524000",
    "completed_date": "2024-08-10T23:11:02.524000"
  },
  {
    "id": "66b7f3866c2fb3b3a7149f4b",
    "content": "本を読む",
    "created_date": "2024-08-10T23:11:02.524000",
    "completed_date": null
  },
  {
    "id": "66b7f3866c2fb3b3a7149f4c",
    "content": "請求書を支払う",
    "created_date": "2024-08-10T23:11:02.524000",
    "completed_date": null
  },
// 省略
```

:::note info
すべての ToDo アイテムを取得できています。
:::

#### GET: /todos/complete エンドポイントの動作確認

完了した ToDo アイテムを取得します。

```sh
$ curl -s http://localhost:5000/todos/complete | jq '.'
```

レスポンス

```sh
[
  {
    "id": "66b7f3866c2fb3b3a7149f48",
    "content": "食材を買う",
    "created_date": "2024-08-10T23:11:02.524000",
    "completed_date": "2024-08-10T23:11:02.524000"
  },
  {
    "id": "66b7f3866c2fb3b3a7149f49",
    "content": "報告書を仕上げる",
    "created_date": "2024-08-10T23:11:02.524000",
    "completed_date": "2024-08-10T23:11:02.524000"
  },
  {
    "id": "66b7f3866c2fb3b3a7149f4a",
    "content": "運動する",
    "created_date": "2024-08-10T23:11:02.524000",
    "completed_date": "2024-08-10T23:11:02.524000"
  }
]
```

:::note info
完了した ToDo アイテムを取得できています。
:::

#### GET: /todos/{id} エンドポイントの動作確認

ID で ToDo アイテムを取得します。

:::note warn
id: 66b7f3866c2fb3b3a7149f4f は、その都度適切な値を設定してください。
:::

```sh
$ curl -s http://localhost:5000/todos/66b7f3866c2fb3b3a7149f4f | jq '.'
```

レスポンス

```sh
{
  "id": "66b7f3866c2fb3b3a7149f4f",
  "content": "コードを書く",
  "created_date": "2024-08-10T23:11:02.524000",
  "completed_date": null
}
```

:::note info
ID で ToDo アイテムを取得できています。
:::

#### POST: /todos エンドポイントの動作確認

新しい ToDo アイテムを追加します。

```sh
$ curl -s -X POST http://localhost:5000/todos \
    -H 'Content-Type: application/json; charset=utf-8' \
    -d \
'{
    "content": "昼寝をする"
}' | jq '.'
```

レスポンス

```sh
{
  "id": "66b800041f565c2b90431961",
  "content": "昼寝をする",
  "created_date": "2024-08-11T00:04:20.451104",
  "completed_date": null
}
```

:::note info
新しい ToDo アイテムが追加できています。
:::

#### PUT: /todos/{id} エンドポイントの動作確認

既存の ToDo アイテムを更新します。

:::note warn
id: 66b800041f565c2b90431961 は、その都度適切な値を設定してください。
:::

```sh
$ curl -s -X PUT http://localhost:5000/todos/66b800041f565c2b90431961 \
    -H 'Content-Type: application/json; charset=utf-8' \
    -d \
'{
    "content": "窓を開ける"
}' | jq '.'
```

レスポンス

```sh
null
```

:::note warn
レスポンスは null ですが、DB のコレクションを確認すると対象のレコードが更新されています。
:::

#### DELETE: /todos/{id} エンドポイントの動作確認

ID で ToDo アイテムを削除します。

:::note warn
id: 66b800041f565c2b90431961 は、その都度適切な値を設定してください。
:::

```sh
$ curl -s -X DELETE http://localhost:5000/todos/66b800041f565c2b90431961 | jq '.'
```

レスポンス

```sh
null
```

:::note warn
レスポンスは null ですが、DB のコレクションを確認すると対象のレコードが削除されています。
:::

:::note info
ここまでの手順で、最小限の **CRUD 操作**を行う **REST API** をアプリに実装できました。
:::

### コンテナイメージの作成

Dockerfile を作成します。

```sh
$ vim Dockerfile
```

ファイルの内容

```Dockerfile:Dockerfile
# set up the container.
FROM python:3.12-slim-bookworm

# set the working dir.
WORKDIR /app

# copy the app dir.
COPY app app

# install libraries.
RUN pip install --no-cache-dir fastapi uvicorn pydantic motor

# expose the port.
EXPOSE 5000

# command to run the app using uvicorn.
CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","5000"]
```

<details>
<summary>説明を開きます。</summary>

|要素|説明|
|:--|:--|
|FROM python:3.12-slim-bookworm|ベースイメージとして、Python 公式 3.12 バージョンの slim-bookworm イメージを使用しています。|
|WORKDIR /app|コンテナー内の作業ディレクトリを /app に設定しています。|
|COPY app app|ホストマシンのカレントディレクトリ内の app ディレクトリを、コンテナー内の /app ディレクトリにコピーします。|
|RUN pip install --no-cache-dir fastapi uvicorn pydantic motor:|必要な Python ライブラリをインストールします。--no-cache-dir オプションはキャッシュを使用しないように指定しています。|
|EXPOSE 5000|コンテナーがリッスンするポート番号を指定します。ここではポート番号 5000 を指定しています。|
|CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","5000"]:|コンテナーが起動した際に、FastAPI アプリケーションを uvicorn サーバーで実行するコマンドを設定しています。--host でホストアドレスを、--port でポート番号を指定しています。|

</details>

Docker デーモンを起動します。

```sh
$ sudo service docker start
```

:::note warn
Docker 環境をお持ちでない場合は、以下の関連記事から Docker Engine のインストール手順をご確認いただけます。
:::

https://qiita.com/studio_meowtoon/items/3bceee25ac67cc73ebbe

コンテナイメージをビルドします。

```sh
$ docker build \
    --no-cache \
    --tag api-todo-fastapi:latest .
```

コンテナイメージを確認します。

```sh
$ docker images | grep api-todo-fastapi
api-todo-fastapi    latest    a41137817bf9    12 seconds ago    163MB
```

:::note info
ここまでの手順で、ローカル環境の **Docker** にアプリの**カスタムコンテナイメージ**をビルドすることができました。
:::

### コンテナーを起動

ローカルでコンテナーを起動します。
※ コンテナーを停止するときは ctrl + C を押します。

:::note alert
コンテナー間通信するために **net-todo** という Docker ネットワークをあらかじめ作成しています。ご注意ください。
:::

```sh
$ docker run --rm \
    --publish 5000:5000 \
    --name api-local \
    --net net-todo \
    --env DB_HOST=mongodb-todo \
    --env DB_PORT=27017 \
    --env DB_NAME=db_todo \
    --env DB_USER=root \
    --env DB_PASSWORD=password \
    api-todo-fastapi
```

:::note info
ここまでの手順で、ローカル環境の **Docker** でアプリの**カスタムコンテナー**を起動することができました。
:::

### コンテナーの動作確認

別ターミナルから curl コマンドで確認します。
※ ID で ToDo アイテムを取得します。

:::note warn
id: 66b7f3866c2fb3b3a7149f4f は、その都度適切な値を設定してください。
:::

```sh
$ curl -s http://localhost:5000/todos/66b7f3866c2fb3b3a7149f4f | jq '.'
```

レスポンス

```sh
{
  "id": "66b7f3866c2fb3b3a7149f4f",
  "content": "コードを書く",
  "created_date": "2024-08-10T23:11:02.524000",
  "completed_date": null
}
```

:::note info
ここまでの手順で、ターミナルにレスポンスが表示され、**JSON データ**を取得することができました。
:::

コンテナーの状態を確認してみます。

```sh
$ docker ps
CONTAINER ID   IMAGE              COMMAND                   CREATED              STATUS              PORTS                                           NAMES
fafc2f542c48   api-todo-fastapi   "uvicorn app.main:ap…"   About a minute ago   Up About a minute   0.0.0.0:5000->5000/tcp, :::5000->5000/tcp       api-local
6b411c62bc95   mongo-base         "docker-entrypoint.s…"   About an hour ago    Up About an hour    0.0.0.0:27017->27017/tcp, :::27017->27017/tcp   mongodb-todo
```

### コンテナーに接続

別ターミナルからコンテナーに接続します。

```sh
$ docker exec -it api-local /bin/bash
```

コンテナー接続後にディレクトリを確認します。
※ コンテナーから出るときは ctrl + D を押します。

```
# pwd
/app
# ls -lah
total 12K
drwxr-xr-x 1 root root 4.0K Aug 11 00:08 .
drwxr-xr-x 1 root root 4.0K Aug 11 00:09 ..
drwxr-xr-x 3 root root 4.0K Aug 10 23:59 app
# cd app
# ls -lah
total 20K
drwxr-xr-x 3 root root 4.0K Aug 10 23:59 .
drwxr-xr-x 1 root root 4.0K Aug 11 00:08 ..
drwxr-xr-x 2 root root 4.0K Aug 10 23:59 __pycache__
-rw-r--r-- 1 root root 2.1K Aug 10 23:55 main.py
-rw-r--r-- 1 root root 2.1K Aug 10 23:54 model.py
```

top コマンドで状況を確認します。

```
# apt update
# apt install procps
# top
top - 00:13:22 up  1:16,  0 user,  load average: 0.07, 0.02, 0.00
Tasks:   3 total,   1 running,   2 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.2 us,  0.1 sy,  0.0 ni, 99.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  15949.2 total,  14772.7 free,    965.5 used,    487.9 buff/cache
MiB Swap:   4096.0 total,   4096.0 free,      0.0 used.  14983.6 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
      1 root      20   0  358404  57232  14220 S   0.0   0.4   0:00.95 uvicorn
     11 root      20   0    4608   3616   3084 S   0.0   0.0   0:00.01 bash
    180 root      20   0    8992   4736   2852 R   0.0   0.0   0:00.00 top
```

コンテナーの情報を表示してみます。

```cs
# cat /etc/*-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
```

:::note warn
このコンテナーは Debian GNU/Linux をベースに作成されています。つまり、Debian GNU/Linux と同じように扱うことができます。
:::

## おまけ：Swagger 装備

:::note info
**FastAPI** には **Swagger が標準で装備**されています。
:::

### Swagger を確認

Web ブラウザで Swagger の URL を確認します。

```sh
http://localhost:5000/docs
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/688bef18-7c19-1160-c2be-3b72dcb47449.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/6d0d71f4-4e3e-0c6c-c27a-24333e9cb732.png)

:::note info
これらのツールを活用することにより、より手軽に API を操作できます。
:::

## まとめ

WSL Ubuntu の Docker 環境で、Python FastAPI の最小 API を実装することができました。

:::note alert
この記事の実装例は1つのアプローチに過ぎず、必ずしも正しい方法とは限りません。他にも多様な方法がありますので、さまざまな情報を照らし合わせて検討してみてください。
:::

どうでしたか？　WSL Ubuntu で、Python FastAPI Web アプリケーションを手軽に起動できます。ぜひお試しください。今後も Python の開発環境などを紹介しますので、ぜひお楽しみにしてください。

## 推奨コンテンツ

https://qiita.com/studio_meowtoon/items/606d9ca1487e6b64a3c6