fly.io にデプロイする Web アプリを開発するためのメモ

趣味で作っている Web アプリの開発中に得られたいろいろをメモ
あとで整理するかもしれないし、しないかもしれない

## `fly.io`
#### コマンドいろいろ
- インストール
    https://fly.io/docs/hands-on/install-flyctl/
    ```shell:macOS
    $ brew install flyctl
    ```

- ログインする
    https://fly.io/docs/flyctl/auth-login/
    ```shell
    $ fly auth login --email [email_address]
    ```   

- ログアウトする
    https://fly.io/docs/flyctl/auth-logout/#usage
    ```shell
    $ fly auth logout
    ```
    どうやらトークンがローカルに保存されているようで、アカウントを切り替えたいときは競合を避けるために、ログアウトしたほうがよいみたい

- `flyctl` のアップデート
    ```shell
    $ fly version update
    ```

- 環境変数の確認
    https://fly.io/docs/flyctl/secrets-list/#usage
    ```shell
    $ fly secrets list
    ```

- アプリのステータスの確認
    https://fly.io/docs/flyctl/status/
    ```shell
    $ fly status
    ```

- アプリ (マシン) の起動
    https://fly.io/docs/flyctl/machine-start/
    ```shell
    $ fly machine start [id]
    ```

- アプリ (マシン) の停止
    https://fly.io/docs/flyctl/machine-stop/#usage
    ```shell
    $ fly machine stop [id]
    ```

- Web アプリを開く
    ```shell
    $ fly open
    ```
    以下の URL になる模様 (無料枠だと開発用ということで `dev` がつく)
    `https://<application-name>.fly.dev/`

- WireGuard
    https://fly.io/docs/flyctl/wireguard/
    ```shell
    $ fly wireguard　[command]
    ```

- `fly.io` 上で立てた DB への接続
    ```shell
    $ fly postgres connect -a [postgres-db-name]
    $ fly proxy 15432:5432 -a [postgres-db-name]
    ```

- `fly.io` 上の DB の `DATABASE_URL` を参照する
    ```shell
    $ fly postgres attach [postgres-db-name] -a [app-name]
    ```
    - こんな感じで出力される
      ```shell
      postgres://[app-name]:XXXXXXXXXXXXXXX@top2.nearest.of.[postgres-db-name].internal:5432/[app-name]?sslmode=disable
      ```
    - `Username: app-name`
    - `Password: XXXXXXXXXXXXXXX`
    だと思われ...
    設定を忘れて、改めてパスワードを参照したいときに使えると思います

- `fly.io` の `image` の更新
    https://fly.io/docs/flyctl/image-update/
    ```shell
    $ fly image update
    ```

#### デプロイ手順
- ローンチ
    デプロイのために設定ファイル (`fly.toml`) を作成する
    ```shell
    $ fly launch
    ```
    ここでアプリ名や DB の設定を行う
    吐き出された `fly.toml` は `Dockerfile` と同じ階層に入れておく

- デプロイ
    ```shell
    $ fly deploy
    ```

- `Procfile`
    Web アプリの実行コマンドを記述する
    ```txt:Procfile
    web: uvicorn main:app --host 0.0.0.0 --port 8080
    ```
    FastAPI ならこんな感じ……らしいが、うまくいかなかった
    `Dockerfile` に直接実行コマンドを書いたら起動できた
    ```dockerfile:Dockerfile
    CMD python main.py
    ```
    こういうこと

## ローカル開発環境構築
Web アプリをつくるのに用意した環境をまとめる
- ディレクトリ構成
    ```txt
    .
    ├── backend
    │   ├── app
    │   │   └── main.py
    │   ├── dev.dockerfile
    │   ├── requirements.txt
    │   └── vue
    │       └── dist/
    ├── frontend
    │   ├── app/
    │   └── dev.dockerfile
    ├── initdb/
    ├── nginx
    │   ├── conf.d/
    │   ├── dev.dockerfile
    │   └── nginx.conf
    ├── Dockerfile
    ├── docker-compose.yml
    └── fly.toml
    ```
    以下にまとめるそれぞれの環境を `docker-compose.yml` でいい感じにする
    詳しくは後述する

### バックエンド
FastAPI で API を用意した
- コンテナ
    ```dockerfile:dev.dockerfile
    FROM python:3.10.8-slim-buster    
    WORKDIR /backend
    
    COPY ./requirements.txt requirements.txt
    
    RUN apt-get update && apt-get clean && apt-get install -y libpq-dev gcc git
    RUN pip install --upgrade pip && pip install -r requirements.txt
    
    COPY . /backend
    ```
    Python のイメージから必要なものを入れているだけ

- `main.py`
    ```python:main.py
    from dotenv import load_dotenv
    from fastapi import FastAPI
    from fastapi.staticfiles import StaticFiles
    from starlette.middleware.cors import CORSMiddleware
    import os
    import uvicorn
        
    app = FastAPI()
    load_dotenv()    
    
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )    
    
    @app.get("/healthcheck")
    def read_root():
        return {"status": "ok"}    
    
    # 環境切り分け
    if os.environ["ENV"] != "dev":
        app.mount("/", StaticFiles(directory="vue/dist", html=True), name="html")    
    
    if __name__ == "__main__":
        uvicorn.run(
            app="main:app",
            host="0.0.0.0",
            reload=True,
            port=8080,
            log_level="debug",
        )
    ```
    - `main.py` を動かすことで `uvicorn` を実行している
    - `vue/dist` ディレクトリに、本番用の静的ファイルを入れているので、実行環境が `dev` でなければそちらを読むように切り分けている

- `requirements.txt`
    ```txt:requirements.txt
    fastapi==0.95.0
    python-dotenv==1.0.0
    uvicorn==0.21.1
    ```
    必要なパッケージを必要なだけ入れる

### フロントエンド
Vue3 で UI をなんとかしているが Vuetify 3 を使いたかったのでちょっと構成が違う
Vuetify 3 の公式にしたがっていれば OK
- コンテナ
    ```dockerfile:dev.dockerfile
    FROM node:19-alpine3.16
    WORKDIR /frontend/app
    
    COPY app /frontend/app
    RUN yarn install 
    ```
    node のイメージから必要なものを入れているだけ

- Vue のプロジェクトの作成
  以下の手順で進めていく
    1. 公式にしたがって Vuetify 3 の環境をつくる
       ```shell
       $ yarn create vuetify
       ```

    1. ホストから見れるようにコンフィグファイルをちょっといじる
       以下を `vite.config.js` に追記する
       ```javascript:vite.config.js
          server: {
              host: true,
              port: 3000,
          }
       ```
       要は `host: true` が大切ってコト

    1. API を叩く先を FastAPI にしてあげる
       以下の内容で `app/src/plugins/axios.js` を作成する
       ```javascript:index.js
       export let axios;        
       export default {
         install(app) {
           // base url
           app.config.globalProperties.$http.defaults.baseURL =
                // 本番環境は、ここが本番環境の URL になっていれば OK
                "http://localhost:8080";
       
           // request interceptor
           app.config.globalProperties.$http.interceptors.request.use((config) => {
             config.headers.Accept = "application/json";
             return config;
           });
       
           // response interceptor
           app.config.globalProperties.$http.interceptors.response.use(
             (response) => {
               return response;
             },
             function (error) {
               return Promise.reject(error);
             }
           );       
           axios = app.config.globalProperties.$http;
         },
         get(url) {
           return axios.get(url);
         },
         post(url) {
           return axios.post(url);
         },
       };
       ```

### Web サーバー
nginx で開発用 Web サーバーを立てる
なくてもなんとかなるが、ホットリロードが効かないなど、あまりうれしくない
- コンテナ
    ```dockerfile:dev.dockerfile
    FROM nginx:1.23

    RUN apt-get update && \
    apt-get install -y vim less
    ```
    nginx のイメージから必要なものを入れているだけ

- ポートの設定
    ```config:nginx.conf
    worker_processes auto;
    
    events {
      worker_connections 512;
      multi_accept on;
    }
    
    http {
      include /etc/nginx/conf.d/server.conf; # 設定ファイルの棲み分け
      server_tokens off;
      send_timeout 60;
    
      # mimetype
      include         /etc/nginx/mime.types;
      default_type    application/octet-stream;
    }
    ```
    ```config:conf.d/server.conf
    server {
        listen 8000;
        location / {    
            root /www/dist;
            index index.html;
        }
    }
    ```
    `8000` 番で受ける

### コンテナ設定
`docker-compose.yml` でこれまでのあれやこれやを立ち上げ、接続する
```yml:docker-compose.yml
version: "3"

services:
  fastapi:
    build:
      context: ./backend
      dockerfile: dev.dockerfile
    environment:
      ENV: dev
    volumes:
      - ./backend:/backend
    tty: true
    command: python app/main.py
    ports:
      - 8080:8080
    restart: on-failure
    networks:
      - dev

  vue:
    build:
      context: ./frontend
      dockerfile: dev.dockerfile
    volumes:
      - ./frontend:/frontend
    tty: true
    command: yarn dev
    ports:
      - 3000:3000
    depends_on:
      - fastapi
      - postgres
      - nginx
    restart: on-failure

  nginx:
    build:
      context: ./nginx
      dockerfile: dev.dockerfile
    ports:
      - 8000:8000
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/log:/var/log/nginx
      - ./frontend/app/dist:/www/dist
    tty: true
    restart: on-failure

  postgres:
    image: postgres:15
    container_name: postgres
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: postgres
    ports:
      - 5432:5432
    volumes:
      - postgres:/var/lib/postgresql/data
      - ./initdb:/docker-entrypoint-initdb.d
    networks:
      - dev
    depends_on:
      - fastapi

volumes:
  django:
  vue:
  nginx:
  postgres:

networks:
  dev:
    driver: bridge
```

- キーポイント
    - それぞれのサービスのポートをホストで受けられるようにする
    - `volumes` でホスト側とファイルを共有する
    - `depends_on` で、他のサービスの処理を待つ
    - 特にバックエンドと DB は `networks` で橋渡しをして、接続できるようにする