🐍

Docker + FastAPI + nginxで環境構築

2023/09/24に公開

背景

業務で今まで触ったことのなかったpythonでapi開発をする必要になったので、同じような人がいた場合参考になればと思い自分が行った環境構築の手順をここに残します。

Pythonのフレームワーク

pythonのどのフレームワークを使って開発をするかからまず決めないといけなかったので、参考までにどういう基準でFastAPIを選んだのか書いておきます。
一応、候補として

  • Django
  • Flask
  • FastAPI

の3種類がありました。
Djangoはフルスタックなフレームワークなので、必要なものが全部揃っているのがメリットですが、今回はサーバーが立てられればいいのでオーバースペックなのと、MVC使いたくないのでナシだなって思いました。
Flaskはマイクロフレームワークで軽量かつカスタマイズが色々できるので迷ったのですが、「Flaskにある機能くらい全部自分で作ればいいなあ」と思い消去法でFastAPIにしました。
FastAPIは、非同期フレームワークなので処理スピードが早いです。業務で重い処理を行う関係上相性がいいなと思いました。また、マイクロサービス化を視野に入れたときに、設計の自由度は自分の中で結構重要な点でした。

メリットとしては、

  • デフォルトでSwaggerが使える
  • 簡単にサーバーが立てられる
  • 非同期通信を簡単に実装できる

デメリットとしては、

  • 比較的新しいフレームワークなのでまだまだ情報が少ない
  • デフォルトでは、サーバーを立てる機能くらいしかない

という感じです。

前提

  • MacOS
  • python3.8
  • FastAPI 0.103.1
  • uvicorn 0.23.2
  • requests 2.23.1
  • nginx 1.21

ディレクトリ構成

├─ docker/
│  ├─ nginx
│  │   ├─ conf/
│  │   └─ default.conf
│  ├─ log/
│  └─  Dockerfile
├─ python
│  └─  Dockerfile
├─ src/
│  └─ main.py
├─ docker-compose.yaml
├─ poetry.lock
└─  project.toml

packageのinstall

今回は、package管理にpoetry(npmのpythonバージョン)を使います。
pipではなくpoetryを使う理由は、バージョン管理です。pipを使っている人が多い印象ですが、pipでrequirement.txtにパッケージを記述していくのもめんどくさいし、バージョン管理もしてくれないので、プロジェクトという観点から考えたらpoetryを使いたいと思いました。
Python3のインストールがまだの方はpython3のインストールから行なって下さい。
まずはpoetryのinstall

pip install poetry

project.tomlを作成します。

poetry init -n

FastAPIでサーバーを立てるのに必要なものを一式installします。

poetry add fastapi uvicorn requests

サーバーを立てるのに必要なのはこれだけです。
次に、src配下にmain.pyを作成します。main.pyはいわゆるルーティングファイルです。
正直、laravelのrouteファイルのような使い勝手は望めません。
簡単な、仮説検証くらいならここに直接ロジックを書いていく感じです。
必要に応じて、handler層(MVCで言うところのController)を設計してあげて下さい。
データ分析などの簡単な仮説検証だけだとそこまでファットにならなさそうなので、handler層だけでUseCase層やDomain層を書かなくても、そこまで開発効率は落ちないかなと書きながら思いました。なので、本当に早くサーバーを立てて開発したいときに向いてると思います。

from fastapi import FastAPI

app = FastAPI(docs_url="/docs")

@app.get('/')
def index():
    return {"message": "Success"}
uvicorn src.main:app --port 8080 --reload

を実行して、localhost:8080にアクセスしてみましょう。
srs.mainの部分は、src/main.pyに対応していて、:appの部分はapp = FastAPI(docs_url="/docs")の部分に対応しています。
--port 8080でport番号を指定しているので、もしport番号が被ってサーバーが立てられなかったら、--port 8081など他の番号に変更してあげましょう。

docs_url="/docs"

はSwaggerのルーティングを指定しています。
localhost:8080/docsでSwaggerにアクセスするという意味で、

app = FastAPI()

でも動きます。デフォルトでは、/docsがSwaggerのルーティングです。
staging、localごとにURLを変えたりもできますし、本番環境で隠すこともできます。

Dockerでコンテナを立てる

これだと他の人がgit cloneをしたときに毎回コマンドを打たないといけない上に、バージョンがlocal環境間で異なると言ったような問題が出てきます。なので、このプロジェクトをコンテナで管理するようにしましょう。

pythonのDockerfile

docker/python配下に以下のDockerfileを作成して下さい。

FROM python:3.8

WORKDIR /var/www/app

COPY ./pyproject.toml /var/www/app/pyproject.toml
COPY ./poetry.lock /var/www/app/poetry.lock

RUN pip install --upgrade pip
RUN pip install poetry
RUN poetry install --no-root

COPY ./src /var/www/app/src

CMD ["poetry", "run", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"]

docker-compose.yaml

src配下にdocker-compose.yamlを作成して下さい。

version: "3.0"

services:
  python38:
    build:
      context: ./
      dockerfile: "./docker/python/Dockerfile"
    container_name: "app_python38"
    working_dir: /var/www/app
    restart: always
    volumes:
      - ./src:/var/www/app/src
   ports:
      - "8080:8080"

イメージをbuild

docker compose build

でimageをbuildしましょう。
buildが終わったら、

docker compose up -d

でコンテナを立てます。
先ほどと同様に、localhost:8080にアクセスできればOKです。

サーバーをnginxで立てる

docker/nginx配下にnginxのDockerfileを作成して下さい。

nginxのDockerfile

FROM nginx:1.21-alpine

# ローカルのdefault.confをコンテナにコピー
COPY docker/nginx/conf/default.conf /etc/nginx/conf.d/default.conf

nginxの設定ファイル

docker/nginx/confにdefault.confを作成して下さい。

# FastAPIの8080番ポートとつなぐ
upstream fastapi {
    # サーバにFastAPIのコンテナ名を指定。app_python38
    # ポートはFastAPIのコンテナの8080番Port
    server app_python38:8080;
}

server {
    # HTTPの80番Portを指定
    # コンテナのnginxのportと合わせる
    listen 80;
    server_name 0.0.0.0;

    # プロキシ設定
    # 実際はNginxのコンテナにアクセスしてるのをFastAPIにアクセスしてるかのようにみせる
    location / {
        proxy_pass http://fastapi;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect off;
    }
}

注意点としては、default.confのportとdocker-composeのport番号を合わせることと、pythonのコンテナに正しくつなぐことです。

次に、docker-compose.yamlを書き換えます。

docker-compose.yaml

version: "3.0"

services:
  python38:
    build:
      context: ./
      dockerfile: "./docker/python/Dockerfile"
    container_name: "app_python38"
    working_dir: /var/www/app
    restart: always
    volumes:
      - ./src:/var/www/app/src
    expose:
      - "8080"
    env_file: ./.env

  nginx:
    image: nginx:1.21-alpine
    container_name: "app_nginx"
    # NginxのDockerfileをビルドする
    build:
      # ビルドコンテキストはカレントディレクトリ
      context: ./
      dockerfile: "./docker/nginx/Dockerfile"
    volumes:
      - ./docker/nginx/conf:/etc/nginx/conf.d
      - ./docker/nginx/log:/var/log/nginx
    restart: always
    depends_on:
      - python38
    ports:
      - "8080:80"
    links:
      - python38

書き変わった点としては、python38のportがexposeに変わっています。
pythonでport番号8080でサーバーを立てていたのに対して、nginxでサーバーを立てるためにexpose 8080でnginxにport8080で接続するように変更してます。
その後、同じようにコンテナを立ててlocalhost:8080にアクセスすれば良いです。

docker compose build
docker compose up -d

localhost:8080にアクセスして、 {"message": "Success"}と表示されていればOKです。

補足:別階層のフォルダをimportしたい時のパスの通し方

pythonの言語仕様では、基本的に同ディレクトリのフォルダをimportすることしか想定されていません。
なので、例えばsrc/app_2のファイルのフォルダをsrc/app_1に呼びたいときに普通にimportしてもエラーを吐きます。
なのでそう言った場合、

ENV PYTHONPATH /var/www/app/src/path_1

のように、通したいpathのディレクトリをpythonのDockerfileに記述してあげます。
仮想環境の外でパスを通すときはexposeのコマンドを直接打ちますが、dockerfileではRUNではなくENVを使います。
複数、pathを通したいときは、

ENV PYTHONPATH /var/www/app/src/path_1:/var/www/app/src/path_2

のように書きます。
これで from path_1.xxx import xxxのように書けます。

Discussion