next.jsアプリをfly.ioにリリースしたので作業をまとめておく

# 何の記事?
herokuが有料化されてから、アプリのホスティングをどこにするかと悩まれている方も多いはずの今日この頃。
かくいう私も趣味サイトのデプロイをどこにしようか迷っていたところ、fly.ioが要件をいい感じに満たしてくれそうだったので、色々と遊びながら、デプロイまで漕ぎ着けてみました。

今回は、fly.ioにデプロイするまでの過程や注意点を放流🐟していこうと思います。

## アプリ構成
- FW: next.js
- ホスティング: fly.io
- データベース: postgres
- ドメイン: muumuu-domain
(重要な部分だけ)

## 前提
[fly.io](https://fly.io/)の会員登録は終わっている
flyctlはインストール済み(ちな、brewでは`brew install flyctl`で一発
next.jsはデプロイできる状態まで準備済み
prisma使ってる

作業ディレクトリは、next.jsのroot
```terminal
❯ pwd # -> /Users/🐱/Desktop/dev/myapp
```

## 実装
fly.ioでアプリを動かすには、flyctlコマンドを通じてデプロイまで操作していきます。
デプロイまでの手順としては、アプリの基礎情報の作成、環境変数の設定、データベースの設定、そしてドメインの設定といったステップで進めます。

### アプリの基礎情報
`flyctl launch`コマンドを実行すると、fly.ioにアプリをアップロードするまでに必要な情報をプロンプトに従って入力することで、デプロイまでを一気に行うことができます。

```terminal
❯ flyctl launch
Creating app in /Users/🐱/Desktop/dev/myapp
Scanning source code
Detected a Dockerfile app
? Choose an app name (leave blank to generate one): myapp # ①
? Select Organization: my-super-secret@email.com (personal) # ②
? Choose a region for deployment: Tokyo, Japan (nrt) # ③
Created app myapp-37 in organization personal
Wrote config file fly.toml
? Would you like to set up a Postgresql database now? No # ④
? Would you like to set up an Upstash Redis database now? No # ⑤
? Would you like to deploy now? No # ⑥
Your app is ready! Deploy with `flyctl deploy`
```
さて、今回はローカルのnext.jsアプリディレクトリから`flyctl launch`を実行して、上記のように設定を行いました。

::: note info
設定を行うと以下の３つのファイルが作成されます
.dockerignore
Dockerfile
fly.toml
:::

#### ①アプリ名の設定
fly.ioに展開するアプリ名です。これはfly.ioで、ホスト名として利用される為、他の人と被らないようにユニークにする必要があるので要注意。

:::note info
アプリ名がhogeであれば[https://hoge.fly.dev](https://hoge.fly.dev)というurlが発行されます。
:::

#### ②組織名
この組織名に関しては、fly.ioのダッシュボードから予めorganizationを作成してあれば、personal以外にも選択が可能になります。今回は、特に指定がないので、脳死で先に進みます。

:::note info
後々、origanizationを変更したいという時は、`flyctl apps move -a myapp -o cat-lovers-organization`とflyctlから変更が可能です。
:::

#### ③リージョン
リージョンは、今回はtokyoを選択。

#### ④postgresql
launchコマンド実行と同時に、postgresのセットアップを一緒に行うかを尋ねられます。
デフォルトのオプションでは以下のように、選択項目が表示されますが、今回は後々設定して作りたいのでNを選択。

```terminal
Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
  Production - Highly available, 2x shared CPUs, 4GB RAM, 40GB disk
  Production - Highly available, 4x shared CPUs, 8GB RAM, 80GB disk
  Specify custom configuration
```

#### ⑤Redis
今回は不要なのでN

#### ⑥直ぐにデプロイするか
後述しますが、環境変数の設定をDockerfileに行わないといけないのでN

### 環境変数の設定
fly.ioでnext.js運用時は、利用する環境変数の種類に応じて設定方法が異なってきます。

#### secretな環境変数
クライアントに表示したくない秘匿性の高い環境変数は、flycltから値をセットします。

```terminal
❯ flyctl secrets set -a myapp MY_SUPER_SECRET_KEY="this is my super secret key" MY_ANOTHER_SUPER_SECRET_KEY=1234
```

#### publicな環境変数
next.jsではクライアントでも参照可能な環境変数にはNEXT_PUBLIC_とprefixをつけてあげるのが必須になっています。（仮にパブリック環境変数と呼ぶことにします。

このパブリック環境変数は前項目(secretな環境変数)で行ったようなflyctlから環境変数に値をセットしても動きません。パブリック環境変数はDockerイメージをビルド時に値として渡しあげる必要があるので、Dockerfileとfly.tomlファイルを編集することで対応していきます。

今回は、NEXT_PUBLIC_FRUITというパブリック環境変数があると仮定し、以下のように各ファイルへ編集を行います。

```toml
# rootディレクトリに作成されたfly.toml
# ...
[build]
  [build.args]
    NEXT_PUBLIC_EXAMPLE = "Value goes here"
    NEXT_PUBLIC_FRUIT = "APPLE"
```

```Dockerfile
    #...
    ARG NEXT_PUBLIC_FRUIT
```

### データベースの設定
今回はデータベースを操作するのにPrismaを利用していると仮定します。
Prismaのデータベースの接続先の値では、schema.prismaファイルで設定してありDATABASE_URLという環境変数を参照している。今回はfly.io上で稼働しているpostgresサーバーを利用するので、postgresサーバーのホスト名を取得する必要があります。
その為、postgresサーバーの作成->urlの取得といった手順を踏んでいきます。

```schema
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
```

#### DBアプリの作成
今回は先ほど、postgresの作成をスキップしたので、flyctlからコマンドを実行してpostgres用マシーンの作成をします。

```terminal
❯ fly postgres create
? Choose an app name (leave blank to generate one): my-sample-db # ①
? Select Organization: my-super-secret@email.com (personal) # ②
? Select region: Tokyo, Japan (nrt) # ③
? Select configuration:  [Use arrows to move, type to filter]
  Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk # 1
  Production - Highly available, 2x shared CPUs, 4GB RAM, 40GB disk # 2
  Production - Highly available, 4x shared CPUs, 8GB RAM, 80GB disk # 3
> Specify custom configuration # ④
  ? Initial cluster size 2
  ? Select VM size: shared-cpu-1x - 256
  ? Volume size 1
```

::: note warn
①、②、③に関してはnext.jsので`flyctl launch`を実行した時と同じなので説明は省略
:::

##### ④設定
postgresマシンスペックの設定項目では、デフォルトで４つの選択項目が存在します。

developmentとproductionの一番大きな違いは、スペック以外にも読み込み、読み書きと専用のクラスターが提供されるということです。(development(1)はクラスターが１つ。production(2)では２つ。(3)は未確認)

さて、どれだけ小規模なアプリでも本番運用を想定すれば、productionを選択したいところですが、ここで選択できるほどのスペックを必要としない場合、無駄にコストがかってしまうのでカスタムを選択することになります。

今回は以下のように設定を進めました。

```terminal
...
? Select configuration: Specify custom configuration
? Initial cluster size 2
? Select VM size: shared-cpu-1x - 256
? Volume size 1
Creating postgres cluster in organization personal
Creating app...
Setting secrets on app my-sample-db...
Provisioning 1 of 2 machines with image flyio/postgres:14.4
Waiting for machine to start...
Machine 3d8d1adwced189 is created
Provisioning 2 of 2 machines with image flyio/postgres:14.4
Waiting for machine to start...
Machine 1781774a932489 is created
==> Monitoring health checks
  Waiting for 3d8d1adwced189 to become healthy (started, 3/3)
  Waiting for 1781774a932489 to become healthy (started, 3/3)

Postgres cluster my-sample-db created
  Username:    postgres
  Password:    mysupersecretpassword
  Hostname:    my-sample-db.internal
  Proxy port:  5432
  Postgres port:  5433
  Connection string: postgres://postgres:mysuperpassword@my-sample-db.internal:5432
```

また、クラスターを複数作成で選択すると「読み書き、読み込みのみ(レプリカ)」といい感じに動作モードを設定してくれるようです。

::: note warn
最後に表示されるユーザー名とパスワードは、大事に保管しておきましょう。
補足しておくと、psql接続以外にもflyctlコマンドでpostgresには接続可能で、自動的にユーザーがpostgresで選択された状態で接続可能です。
:::

### 動作確認
今回はクラスターを２個としたので、動作確認で作成したデータベースに接続し、読み書きを両方で行ってみます。

今回作成したサーバーの情報
| --- id --- | --- モード --- | --- ポート --- |
|---|---|---|
| 3d8d1adwced189 | 読み書き | 5432 |
| 1781774a932489 | 読みのみ | 5433 |


flyctlでは`flyctl postgres connect -a <postgres-app-name>`のコマンドでも接続が可能ですが、flyctlでproxyを通すことで、psql接続も可能になります。

今回は、せっかくなのでproxyを通して接続してみたいと思います。

::: note warn
test_dbというのを予め作成、fruitテーブルを準備してあります。
:::

```terminal
# terminal 1
# ローカルのdbポートと喧嘩するので適当に5455にバインドしてます

❯ flyctl proxy 5455:5432 -a my-sample-db
```
```terminal
# terminal 2

❯ psql postgres://postgres:mysupersecretpassword@localhost:5455/test_db
...

psql (14.6 (Homebrew), server 14.4 (Debian 14.4-1.pgdg110+1))
Type "help" for help.
test_db=# insert into fruit values ('lemon');
INSERT 0 1
test_db=# select * from fruit;
 name  
-------
 lemon
(1 row)
```
問題なく書き込みができていることが確認できました。
では、次にもう一つのサーバーの動作を確認してみましょう。ポート5433に接続し書き込み失敗すれば意図した通りの動作です。

```terminal
# terminal 1
❯ flyctl proxy 5455:5433 -a my-sample-db
Proxying local port 5455 to remote [my-sample-db.internal]:5433
```

```terminal
# terminal 2
❯ psql postgres://postgres:mysupersecretpassword@localhost:5455/test_db
psql (14.6 (Homebrew), server 14.4 (Debian 14.4-1.pgdg110+1))
Type "help" for help.
...
test_db=# insert into fruit values('apple');
ERROR:  cannot execute INSERT in a read-only transaction
test_db=# select * from fruit;
 name  
-------
 lemon
(1 row)
```
前途した通り動作していることが確認できました。

#### prismaから繋がるように
Prismaからpostgresサーバーに繋がるようにするには、前途した通りDATABASE_URLにpostgresのアドレスを指定しないと動作しません。このアドレスですが、基本的には先ほどのDBを作成した際に出力される`Hostname:    my-sample-db.internal`で出力されたホストを指定することになります。

```terminal
❯ flyctl secrets set -a myapp DATABASE_URL="postgres://postgres:mysupersecretpassword@my-sampledb.internal:5432/test_db"
```

さて、ここまで来れば`flyctl deploy`コマンドを実行してアプリをデプロイしておきましょう。


### ドメインの適用
今回は大昔から運用している個人アプリで、ドメインをmuumuuドメインで取得していたので、muumuuドメインを例に説明しています。

#### デプロイしたnext.jsアプリのip取得
fly.ioのダッシュボードから確認することも可能ですが、flyctlでサクッとDNS設定に必要なipを表示します。

```terminal
❯ flyctl ips list -a myapp
VERSION IP                      TYPE    REGION  CREATED AT           
v4      123.456.789.xx          public  global  2022-11-26T12:29:27Z
v6      xxxx:xxxx:z::xxxx       public  global  2022-11-26T12:29:32Z
```
ここで取得したものを、DNSレコードに適応しておきます。ちなみに、muumuuドメインはこんな感じのフォームが用意されてます。

| サブドメイン | 種別 | 内容 | 優先度 |
| -- | -- | -- | -- |
| 空 | A | 123.456.789.xx | |
| 空 | AAAA | xxxx:xxxx:z::xxxx | |

設定を反映したら、あとはflyctlから証明書を発行するだけです。ちなみに、2つ目のコマンドは証明書が発行されたかどうか確認するコマンドなのでやらなくても良いです。

```terminal
❯ flyctl certs create -a myapp example.com
❯ flyctl certs show example.com
```

あとは、反映されればfin!

## おまけ

#### サーバー側で何かしらする時に権限エラーが出る
例えば一時ファイルを作った時とか。
サーバーのコード実行時のユーザーはnodeなのでDockerイメージ生成時のファイルコピー時に、nodeに権限を付与しておきましょう。

```Dockerfile
# ファイルコピー時にnodeに権限付与
COPY --from=builder --chown=node:node /app ./
```

#### デプロイできない

色々遊んでいると`"ENOSPC: no space left on device, write"`的なエラーが出たりして、デプロイできない問題にちょくちょくぶち当たる。これは、`fly-builder-xxx`という名前のビルド専用アプリが、過去に作ったdockerイメージの容量で圧迫されて限界がきて発生しているっぽいです。

ということで、このビルド専用インスタンスを作り直す（もしくは、アプリに接続して不要なイメージを削除🤔?。 シンプルに削除するには、ダッシュボードからアプリ一覧を開くと`free builder`タグがついてるものがあるので、その設定からdeleteを実行する。

また、このビルド専用アプリは`flyctl deploy`コマンドを実行すると勝手に作成されるので、特別、自身で作成しないといけない。なんてことはないのでご安心ください。