FastAPI + uvicorn + nginxをdocker-composeで構築

# 目標

- docker-composeを使い、NginxのリバースプロキシによってFastAPI（uvicornで起動）のアプリをhttpで公開する
    - まずは認証無しのhttp（80ポート）で公開

# ファイル構成

```bash:shell（bashなど）
$ tree  
.
├── app
│   ├── Dockerfile
│   ├── app
│   │   ├── __init__.py
│   │   └── main.py
│   ├── poetry.lock
│   └── pyproject.toml
├── docker-compose.yml
└── web
    └── conf.d
        └── app.conf
```
- FastAPIアプリは`app/Dockerfile`を使ってイメージを作成
    - ソースコードは`app/app`内に格納
    - Pythonのパッケージ管理は`pyproject.toml`と必要であれば`poetry.lock`を使っている([Poetry](https://github.com/python-poetry/poetry)使用)
        - （別に深い理由も無いので簡単に[`pip + requirements.txt`](https://pip.pypa.io/en/stable/reference/pip_install/#example-requirements-file)でやったり[`Pipenv`](https://pipenv-fork.readthedocs.io/en/latest/)や[`miniconda`](https://docs.conda.io/en/latest/miniconda.html)を利用するなど色々やり方があると思います）
- Nginxの設定は`web/conf.d/app.conf`で行っている

## `docker-compose.yml`

- システム構成の概要について

```yaml:docker-compose.yml
version: '3'

services:
  web:
    container_name: web
    image: nginx:alpine
    depends_on:
      # `app`サービス（コンテナ）が先に起動している必要があるので`depends_on`を指定しておく
      - app
    ports:
      # ポートマッピング： "ホストOSのポート:コンテナ(Nginx)のポート"
      - "80:80"
    volumes:
      # volumeマウント： "ホストOSのパス:コンテナにおけるパス"
      - ./web/conf.d:/etc/nginx/conf.d
    networks:
      - nginx_network

  app:
    container_name: app
    image: test_fastapi_app # ビルドされるDockerイメージ名を指定
    build:
      context: ./app
      dockerfile: Dockerfile
    expose:
      - 8000
    networks:
      - nginx_network
    # 例えばソースコードをリアルタイムに編集したいときは`volumes`でマウントすると便利
    # volumes:
    #   - ./app/app:/app/app
    # appコンテナの`CMD`を上書きするには`command`を使う
    # command: "uvicorn app.main:app --reload --host 0.0.0.0"

networks:
  nginx_network:
    driver: bridge
```

- Nginxのコンテナ`web`とFastAPIアプリのコンテナ`app`を別個に作り、docker-composeのnetworks:`nginx_network`で接続している
- nginxのイメージは軽量なalpineバージョンを使っている


`app`および`web`の中身の詳細は後述。

## `app` (FastAPIアプリ本体)

#### Dockerfile

```dockerfile:Dockerfile
ARG BASE_IMAGE=python:3.8-slim-buster                                                                                
FROM $BASE_IMAGE                                                                                                     
                                                                                                                     
# system update & package install                                                                                    
RUN apt-get -y update && \                                                                                           
    apt-get install -y --no-install-recommends \                                                                     
    build-essential \                                                                                                
    openssl libssl-dev \                                                                                             
    && apt-get clean \                                                                                               
    && rm -rf /var/lib/apt/lists/*                                                                                   
                                                                                                                     
# Add Tini                                                                                                           
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]

# 一般ユーザーアカウントを追加
ARG USER_NAME=app
ARG USER_UID=1000
ARG PASSWD=password

RUN useradd -m -s /bin/bash -u $USER_UID $USER_NAME && \
    gpasswd -a $USER_NAME sudo && \
    echo "${USER_NAME}:${PASSWD}" | chpasswd && \
    echo "${USER_NAME} ALL=(ALL) ALL" >> /etc/sudoers

# FastAPIのソースコードなどをコンテナ内へCOPY
COPY ./app /app/app
COPY ./pyproject.toml /app/pyproject.toml
# local環境で使用しているpoetry.lockファイルがあればそれも追加する
# COPY ./poetry.lock /app/poetry.lock
RUN chown -R ${USER_NAME}:${USER_NAME} /app


USER $USER_NAME
WORKDIR /app
# pip & poetry
ENV PATH $PATH:/home/${USER_NAME}/.local/bin
RUN python3 -m pip install --user --upgrade pip && \
    python3 -m pip install poetry --user && \
    poetry config virtualenvs.in-project true && \
    poetry install && \
    rm -rf ~/.cache/pip/* && \
    rm -rf ~/.cache/pypoetry/*

# Poetryで作った仮想環境にPATHを通しておく（予めpoetryのconfigを設定して仮想環境のPATHを一意にしておく）
ENV PATH=/app/.venv/bin:$PATH

# Configration
EXPOSE 8000

# Execute
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
```
- とりあえずPythonのバージョンは3.8を使用(公式イメージの`slim-buster`系(debianベース)のタグを利用)
- debianベースなので`apt`で必要なライブラリをインストール
- 個人的にrootユーザーで起動したくないので、アプリを起動させる用のユーザーを作成
- [`tini`](https://github.com/krallin/tini)は別に無くても動くが、containerを起動するときに使うとベターらしいので[GitHubでの説明](https://github.com/krallin/tini#using-tini)にしたがって入れておく
- コンテナの`/app`ディレクトリ上にFastAPIアプリ用のファイル一式を格納し、WORKスペースにしている
    - ソースコードの本体は`/app/app`
- ローカルユーザー権限で`Poetry`を使って必要なパッケージをインストールしている
    - `poetry config virtualenvs.in-project true`によって`/app/.venv`に仮想環境が作られるようにしている
    - ローカル環境における開発などで`poetry.lock`ファイルがある場合、`# COPY ./poetry.lock /app/poetry.lock`のコメントアウトを外せばインストールするパッケージを固定出来る


#### `pyproject.toml`

- インストールするパッケージの情報など
    - `tool.poetry.dependencies`に必要最低限なパッケージ情報を記述していて、後は適当です
    - （必要に応じて[公式ドキュメント](https://python-poetry.org/docs/)などを参照して追記・修正していく）

```toml:pyproject.toml
[tool.poetry]
name = "test_fastapi_app"
version = "0.1.0"
description = "just for test"
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "^3.8"
uvicorn = "*"
fastapi = "*"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
```

#### FastAPIアプリ

- 今回はごく最低限だけ準備

`app/app/main.py`は以下のようにしてある：

```python:main.py
"""
app main
"""

from fastapi import FastAPI

app = FastAPI()


@app.get('/')
async def site_root():
    """root"""
    return {"message": "Hello, WORLD!"}
```

- (`app/app/__init__.py`は空ファイル)


`app/app/`以下はそのときどきの目的・必要性に応じて今後作り込んでいく

##### `uvicorn`

FastAPIアプリの起動には、`uvicorn`（[公式ドキュメント](https://www.uvicorn.org/)）を使って、

```bash
uvicorn app.main:app --host 0.0.0.0
```

のようにする。（ディレクトリ名やファイル名が変わると`app.main:app`の部分も変わるはずなので注意する）
オプション：`--host 0.0.0.0`はコンテナ上で起動しているuvicornサーバーを外部から見れるようにするために必要。
uvicornサーバーはデフォルトだと8000ポートで起動するので、`EXPOSE 8000`などを`Dockerfile`や`docker-compose.yml`上でやっておく。（もしかすると要らなかったかもしれない）

なお、`--reload`オプションをつけるとファイルが変更されたときにuvicornサーバーが検知して再起動するようになるので、開発時などに便利

## `web` (NginxによるWebサーバー)

- `app`コンテナで立ち上げているuvicornサーバーのリバースプロキシ
- `/etc/nginx/conf.d/***.conf`に設定を書くことで各種設定が出来る
    - `***.conf`の名前は割となんでも良いらしい（今回は`app.conf`にしている）


```conf:app.conf
upstream backend {
    server app:8000;
}

server {
    listen 80;
    # server_name  localhost;

    location / {
        proxy_pass http://backend;
    }

    # log
    # access_log /var/log/nginx/access.log;
    # error_log /var/log/nginx/error.log;
}
```

- `upstream`のところで`app:8000`のリバースプロキシの設定をしている
    - ここでの`app`は`docker-compose.yml`上で書いたservice名に対応していた気がする（違ってたらすみません）
    - `location /`のところで`app:8000`の内容をNginxの`/`以下で公開している
        - 結果としてNginxの80ポートにアクセスするとFastAPI（uvicorn）の8000ポートが公開している内容が見れることになる
- `server_name`やログ出力が必要だったら適宜該当箇所をコメントアウトする


# 実行

- よく使うもの：

```bash
# `app`のイメージをビルド
docker-compose build

# サービス全体の起動
docker-compose up -d

# 終了時
docker-compose down

# コンソール出力の確認
docker-compose logs
docker-compose logs app # appサービスのログのみ確認

# appコンテナの中に入る
docker-compose run app bash
```

うまく設定ができていれば、`docker-compose up -d`をして http://localhost および http://localhost/docs などにアクセスすると、

- http://localhost （ローカル環境で実行時）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/520143e2-e91c-da99-0003-5111f6587ea5.png)

- http://localhost/docs （ローカル環境で実行時）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680173/708df3c9-77e2-dd28-cd8b-60e99f76cb2e.png)

のような画面が確認出来る。
AWSのEC2など、リモート環境で実行しているときは`localhost`を実行しているマシンのアドレスに適宜置き換えればOKのはず。


# まとめ

なるべくミニマムな内容でタイトルのような構成を作った。
今回はやっていないがNginxを使ってSSL化（HTTPS通信）やBASIC認証などをかけられる。
→ なお、Jinja2のTemplate機能を使っているときに、SSL化で結構ハマって手こずった（httpsがhttpになったり、デフォルト以外のポートを使うときにポート番号情報が欠落したりしてリンク機能がうまく働かなくなったりした）ので、別途まとめた~~る予定~~。（下記↓）

1. [FastAPI + uvicorn + NginxでWebページを表示（Jinja2によるTemplates機能 ）](https://qiita.com/junkor-1011/items/0a1a2a486a779461e757)
2. [FastAPI + uvicorn + NginxでWebページを表示（SSL/HTTPS化 ）](https://qiita.com/junkor-1011/items/f7ff31b7df94e623e6b7)