Turborepo + PNPM + Remixで構成されたアプリケーションをFly.ioにデプロイする

Turborepoでmonorepo構成で、パッケージマネージャーはpnpm、アプリケーションのフレームワークにはRemixを採用したアプリケーションをFly.ioにデプロイするのに苦労しました。
Fly.ioのRun a Remix Appや、Monorepo and Multi-Environment Deploymentsなどの記事もあるし、なんか情報あるだろうと思っていたら全然なかった...。
サンプル
サンプルコードを用意しました。
polidog/turborepo-pnpm-remix-fiyio-deploy-example
そもそもflyioがpnpmに対応していない
flyioにはfly(flyctl)コマンドが用意されています。
たとえば apps/web というremixアプリを作成して、fly launchします。
$ cd apps/
$ pnpm dlx create-remix@latest web
$ fly launch
fly launchを実行すると以下のファイルが作成されます。
- dockerignore
- fly.toml
- Dockerfile
ここで生成されたDockerfileはremixアプリの実行環境なわけですが、pnpmには対応できてません。
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=18.4.0
FROM node:${NODE_VERSION}-slim as base
LABEL fly_launch_runtime="Remix"
# Remix app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV=production
# Throw-away build stage to reduce size of final image
FROM base as build
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install -y python-is-python3 pkg-config build-essential
# Install node modules
COPY --link package.json .
RUN npm install --production=false
# Copy application code
COPY --link . .
# Build application
RUN npm run build
# Remove development dependencies
RUN npm prune --production
# Final stage for app image
FROM base
# Copy built application
COPY --from=build /app /app
# Start the server by default, this can be overwritten at runtime
CMD [ "npm", "run", "start" ]
おそらく flyctl/scanner/node.go のこのあたりのコードをみるとpnpmではなくyarn or npmなのでおそらく対応できてないかなと思います。
https://github.com/superfly/flyctl/blob/c37a567828b464eb097defdf01f30ef0bb732884/scanner/node.go#L83-L93
しかもこれだとmonorepoにも対応できてません。
Dockefileを自分で用意する
ということで、自分でDockerfileを修正していくことにしました。 ベースはfly launchで生成されたdockerfileを修正しています。
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=18.4.0
FROM node:${NODE_VERSION}-slim as base
ARG SCOPE
LABEL fly_launch_runtime="Remix"
# ENV NODE_ENV=production
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install -y python-is-python3 pkg-config build-essential
ENV PNPM_HOME="/root/.local/share/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm i -g pnpm
RUN pnpm add turbo -g
WORKDIR /app
COPY . .
# setup
FROM base as setup
ARG SCOPE
RUN turbo prune --scope=web --docker
# Install & Build packages
FROM base as build
COPY .gitignore .gitignore
COPY --from=setup /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=setup /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
RUN pnpm install --prod=false
COPY --from=setup /app/out/full/ .
COPY turbo.json turbo.json
RUN pnpm run build --filter=web
# Runner
FROM base as runner
ARG SCOPE
ENV NODE_ENV=production
COPY .gitignore .gitignore
COPY --from=build /app/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=build /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
RUN pnpm install --prod --filter=web
COPY --from=build /app/apps/web ./apps/web
WORKDIR /app/apps/web
CMD [ "pnpm", "run", "start", "--filter", "web" ]
修正内容
今回はbase,setup,build,runnerという4つのステージを用意してます。
| ステージ | 内容 |
|---|---|
| base | 全体のイメージ共通での設定 |
| setup | パッケージインストール、ビルド前にすること |
| build | パッケージインストール、ビルド |
| runner | 実行環境 |
という形で分けています。
turbo pruneについて
setupのステージで turbo prune --scope=web --docker コマンドを実行してプルーニングしています。
プルーニングとは?
プルーニングとは、ビルド キャッシュの過成長を効率的に自動的に削除するプロセスです。プルーニングは、ロック ファイル内の依存関係のプルーニングを含め、monorepo のサブセットを作成します。
出典: What is Turborepo and Why Should You Care?
turbo pruneの場合、指定したscopeに最適化されたmonorepoが生成されます。
試しに手元で実行してみると outというディレクトリが作成されます。
$ pnpm dlx turbo prune --scope=web
apps/web が依存しているパッケージを中心にmonorepoがoutディレクトリの中に生成されています。
dockerオプション
turbo pruneにはdockerオプションがあります。 これはDocker環境でビルドするときにDockerのキャッシュを最適にするために必要なオプションになります。
キャッシュの効率化については、以下の記事を参考にしていただければと思います。
- [How can I use the cache efficiently?](https://docs.docker.com/build/cache/#order-your-layers_
- Docker レイヤー キャッシュ(DLC) で高速なDockerビルドを理解。
雑に言ってしまえば、変更が入りやすいものはなるべくDockerfileの後ろにもっていくほうがいいということでいいのかな?
パッケージインストールまでを先に行って、ソースコードのCOPYを行うことで効率的にDockerのキャッシュが使えるということかと思います。
Runnerステージについて
一度Buildしてしまえばnode_modulesはいらなくなるのかなと思っていましたが、そうでもなかったです。
flyioの場合 remix-serve build がproductionで実行されますが、結局nodeのランタイムで動くのでnode_modulesが必要になってしまいます。
ただしruntimeに必要なものだけになるのでdevDependenciesが必要なわけではないので --prod オプションを付けています。
NODE_ENV=prodctionなんでいらない気もしますが、わざと明示的につけています。
RUN pnpm install --prod --filter=web
サーバ側のランタイムで必要なnode_modulesだけをインストールしたかったんですが方法が見つからないので諦めました。
.dockerignoreについて
プロジェクトルートにdockerignoreを配置する必要があります。
node_modulesをignoreしたいのですが (apps|packages)/xxxx/node_modules を拒否したいの ** を使って指定します。
(.gitignoreとちょっと違うの辛い...)
**/node_modules
**/fly.toml
.git
node_moduelsをignoreしないとビルド途中にエラーになります。
ERROR: failed to solve: cannot copy to non-directory:
デプロイする
プロジェクトルートからfly deployコマンドを実行します。
$ fly deploy --config apps/web/fly.toml --dockerfile apps/web/Dockerfile --remote-only
Dockerイメージのサイズについて
どうしてもDockerイメージが1GBを超えてしまう。。。どうにかならないものか。。。。
最後に
自分のフロントエンド力とかDocker力が低いのでまだまだ改良の余地があるかと思います。
ただ現状これでfly.ioで動かすことはできました。
変更したほうが良い箇所があったら、教えていただけると本当に助かります。