Next.js + Prisma の Docker を OpenSSL 3.0 対応する

## はじめに
開発マシンを Intel Mac から M2 Mac に切り替えたら、色々問題が起きた。これまで、[サンプル](https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile)を元に適当にやっていたが、OpenSSL 1.1.1 のサポートが9月11日に終了するので、OpenSSL 3.0 対応を含めて解消しつつ、きちんと Docker と向き合うことにした。

最終的にやりたいことは、ローカルで build した image を AWS ECR に push し、それを AWS ECS に deploy したい。

結論だけ > [TAKT-R-D / nextjs-prisma3-busterslim-openssl3](https://github.com/TAKT-R-D/nextjs-prisma3-busterslim-openssl3)



## 対応方針

対応方針は、Prisma のバージョンによって変わってくる。

本当は、Prisma のバージョンを上げるのが良いと思うが、対応・検証に時間がかかるし、色々考えているうちに、一昨日 [5.0.0](https://github.com/prisma/prisma/releases/tag/5.0.0) がリリースされて絶望したので、いったん Prisma の Upgrade は先延ばしにする一次対応とする。

まず、SSL 3.0.x に対応した Prisma の binaryTarges を確認する。

https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#binarytargets-options

node のバージョンも色々だが、ここでは説明簡略のため 18.16.1 とする。

### Prisma のバージョン >= 4.10.0 の場合
base となる node イメージは、```node:18.16.1-alipine3.17``` とし、```schema.prisma``` の binaryTargets は、```linux-musl-openssl-3.0.x```, ```linux-musl-arm64-openssl-3.0.x``` を追加する。```native``` とそれ以外はいらない。

このケースは、たぶん特筆することはないと思われるので、今回はやらない。時間があったらやってみる。



### Prisma のバージョン < 4.10.0 の場合
この場合、alpine が使えない。イメージはどれでも良いが、なるべく安定していて、軽量にしたいので、Debian の現LTS（2024年6月30日まで）の Buster (Debian 10) の slim な ```node:18.16.1-buster-slim``` を使うことにする。

binaryTargets は、M2 Mac 用に ```linux-arm64-openssl-3.0.x``` (Linux (most distributions except Alpine), ARM64), ECS 用に ```debian-openssl-3.0.x``` (Linux (Debian), x86_64) を入れる。

ECS に Deploy するには、docker image を build する際に ```platform=linux/amd64``` というオプションをつける必要があるが、このオプションをつけると、build する環境（Intel Mac or M2 Mac）に関わらず、実際に使われるイメージは、x86_64 になるので、```debian-openssl-3.0.x``` が必要になる。

また、buster-slim には OpenSSL がインストールされておらず、apt でインストールできる OpenSSL のバージョンは、現時点では、```1.1.1n-0+deb10u3``` であり（[こちら](https://launchpad.net/debian/buster/+source/openssl)）、3.0 は apt ではインストールできないため、ソースコードを取ってきて、make install する必要がある。そのうち apt-get install できるようになると思うので、以下を読む前に、できるかできないかは確認して欲しい。なぜなら悶絶するくらい build に時間がかかるので。

## Debian 10 buster-slim に OpenSSL3.0 をインストールする

まず、インストールするバージョンの検討であるが、[OpenSSLのページ](https://www.openssl.org/source/)を見ると、3.0 系は 3.0.9、3.1 系は 3.1.1 が最新となっている。ちょっと悩ましいところだが、今回は FIPS Validated な最新 3.0.8 を入れることにする。どれでも手順は一緒だと思うので、入れたいものを選んで貰えればよい。

とりあえず、OpenSSL3.0 をインストールする Dockerfile を作る。

```Dockerfile
FROM node:18.16.1-buster-slim AS base

FROM base AS deps
# 何も入ってないので、必要なものを入れる
RUN apt-get update && apt-get install wget build-essential zlib1g-dev make -y

# source を取ってくる。予めローカルにダウンロードしておいて、COPY でも良い
WORKDIR /usr/local/src
RUN wget https://www.openssl.org/source/openssl-3.0.8.tar.gz
RUN tar -zxf /usr/local/src/openssl-3.0.8.tar.gz

WORKDIR /usr/local/src/openssl-3.0.8
RUN ./config --prefix=/usr/local/ssl --openssldir=/usr/local/ssl shared zlib
RUN make && make install

# build
FROM base AS builder
COPY --from=deps /usr/local/ssl /usr/local/ssl
RUN echo "/usr/local/ssl/lib\n/usr/local/ssl/lib64" > /etc/ld.so.conf.d/openssl-3.0.8.conf && ldconfig
ENV PATH="$PATH:/usr/local/ssl/bin"
```

ここで注意点としては、最後から2行目の

```Dockerfile
RUN echo "/usr/local/ssl/lib\n/usr/local/ssl/lib64" > ...
```

make install した際に、ARM64 では、```/usr/local/ssl/lib``` に、x86_64 では ```/usr/local/ssl/lib64``` とパスが変わるため、2行書いている。

```text:/etc/ld.so.conf.d/openssl-3.0.8.conf
/usr/local/ssl/lib
/usr/local/ssl/lib64
```

```ldconfig``` した際にないものは無視されるが、エラーにはならないので、これで。Dockerfile を分けるべきな気もするが横着した。気になる人は、分けてください。


PATH は、```ENV``` としているが、 

```Dockerfilekerfile
RUN echo "$PATH:/usr/local/ssl/bin" > /etc/environment && . /etc/environment
```

などでも良いはず。もっと良い方法もありそうだが、私の Debian 力ではいまはこれが精一杯。

なお、```make```, ```make install``` はめちゃくちゃ時間がかかるので、実運用では、```/usr/local/ssl``` ディレクトリはローカルから ```COPY``` が良いのかも知れないが、本旨からはそれるので、後々考えることとする。

ここまでできれば、あとつまるところはないはず。

runner にて ```addgroup``` か ```groupadd``` かなどあるが、たいした問題ではない。

## 成果物

というわけで、[サンプル](https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile)を元に、image を alpine から buster-slim に変更し、OpenSSL3.0.8 に対応した Next.js + Prisma が動く Dockerfile と schema.prisma は以下のようになる。PORT はお好きに。サンプルは 3000 だ。

イメージサイズは、まだまだチューニングの余地がありそうだが、まぁ良いか…というレベルで、アプリを載せて、alpine の 2 倍、slim じゃない buster の 1/3 程度で、ECR 上で 130 MB くらいだったので、暇があったら検討するかも案件。

なお、じゃあこれを Production 用に使って良いか？というと、それはまた別の問題で、以下の Dockerfile のサンプルが、御社のセキュリティ要件を満たしているかは検証が必要だ。

以下のようになると書いたが、ふと我に返って、github に置いた。

> [TAKT-R-D / nextjs-prisma3-busterslim-openssl3 > Dockerfile](https://github.com/TAKT-R-D/nextjs-prisma3-busterslim-openssl3/blob/main/Dockerfile)

ローカル用 build
```bash
$ docker build ./ -t {your_container_name}
```

ECS 用 build
```bash
$ docker build --platform linux/amd64 ./ -t {your_container_name}
```

builder の ```yarn build``` でこける場合、```prisma generate``` が通ってて、ローカルで build できるなら、```yarn build``` できるので、再度 docker build しましょう。なんでこけることがあるのかは、まだ分かってない。

```text:schema.prisma
generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"]
}
...
```

なお、next.js の docker で、

```Dockerfile
ENV NEXT_TELEMETRY_DISABLED 1
```
は、匿名情報収集を許可しないという設定で、yarn build 中に

```
Attention: Next.js now collects completely anonymous telemetry regarding usage.
```

と、完全に匿名だから大丈夫的なメッセージが出てくるが、情報提供して良いかはアプリケーションのオーナーが決めるべきことだし、Next.js の発展にどうしても貢献したい！という情熱をお持ちでない限り、DISABLED にしておくのが無難だ。


## おまけ

Intel Mac から M2（/M1） Mac に切り替えた際に詰まる点は、上記以外にもいくつかあった。

### node, yarn のバージョン

ローカルの ```yarn``` のバージョンがいつから ```4.0.0-rc.42``` だったか記憶にないが、alpine でも buster-slim でも、```yarn``` のバージョンは、```1.22.19``` である。4と1はだいぶ違うので、```--fronzen-lockfile``` する際にうまく行かない。

というわけで、Project ごとに node, yarn のバージョンは定期的に見直す前提で、固定した方が間違いがない。Intel Mac では node のバージョン管理には、nodebrew を利用していたが、volta に切り替えた。

インストール方法は各自ググっていただくとして、プロジェクトごとに、

```bash
$ volta pin node@18.16.1
$ volat pin yarn@1.22.19
```
などとすると、バージョンを固定できる。package.json に、

```json:package.json
{
  ...,
  "volta": {
    "node": "18.16.1",
    "yarn": "1.22.19"
  }
}
```
こんなものが追記される。
もちろん、volta を利用していなければ、無視される設定ではあるが、便利である。

### 実行 platform
ECS に Deploy する場合、platform=linux/amd64 にしないといけない。docker build の場合は前述の通りで、docker compose の場合は、以下のようにする

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

services:
  app:
    build:
      context: ./
      dockerfile: Dockerfile
    image: app_local
    platform: linux/amd64
    ports:
      - '8080:8080'
```

### インストールできない package もある。

```bycrypt``` など、M1/2 Mac ではインストールできない package もある。そのうちできるようになるかも知れないが対応が必要だ。ググると無理矢理インストールする方法などが出てはくるが、今回は、代替 package を使うことにした。```bycryptjs``` 。大した使い方はしていなかったので、きちんと違いを読み込んではいない。

### ChatGPT に聞くと…

動かなそうだし、試していないが、割りと的を得た回答だった。今どきは、色々ググって調べるよりも、ここからスタートするのも良いかも知れない。


（おしまい）