本稿では Nx を使ったAngular+Nest.jsのmonorepoアプリケーションを単一のDockerイメージとしてデプロイ可能にする手順を記す。
サンプルコードの最終的な状態は以下のリポジトリに公開している。
https://github.com/lacolaco/nx-angular-nest-docker-example
0. 背景
一般的にはSingle Page Applicationのクライアントとサーバーは別のコンテナとしてデプロイされNginxなどで静的ファイルへのリクエストとAPIリクエストを振り分けることが多いが、今回はあえて単一のDockerコンテナでHTTPサーバーを立ち上げ、そのサーバーから静的ファイルとしてクライアントをサーブする。 なぜかというと、今回はGoogle Cloud Runにアプリケーションをデプロイすることが目的だからだ。Cloud Runには単一のDockerfileからビルドされるイメージをデプロイできる。
利点としては、まず環境がCloud Runだけで完結すること。そしてクライアントとサーバーあわせたEnd-to-Endの整合性がとりやすいことがある。 欠点としては個別にデプロイできないことや静的ファイルに特化した配信戦略を取りにくいことだ。ただしCloud RunはFirebase Hostingと連携することでGoogleのCDN網を利用できるため、静的ファイルに適切なレスポンスヘッダをつければFirebase Hostingと同じCDNでのキャッシュが機能する。
1. workspaceの作成
まずはNxのworkspaceを作成する。 create-nx-workspace
コマンドを使い、今回は angular-nest
を選択するが、本稿の趣旨においては別に react-express
でも違いはない。
$ npx create-nx-workspace@latest angular-nest-docker-example`
2. outputPathの変更 (client, server)
angular-nest
テンプレートのデフォルトで生成されるプロジェクトは、クライアントサイドが <application-name>
、サーバーサイドが api
となっている。これをそれぞれ dist/apps/client
と dist/apps/server
にビルド結果が出力されるようにする。


3. @nestjs/serve-static
の追加 (server)
クライアントサイドをビルドした生成物をNest.jsがサーブできるように、静的ファイル配信用のモジュールを追加する。
$ yarn add @nestjs/serve-static`
静的ファイルはさきほど変更したoutputPathのとおり、clientとserverが隣接するディレクトリになるため、ルートディレクトリには ../client
を指定する。 /api*
はAPIのエンドポイントにマッチするように除外しておく。
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import * as path from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: path.join(__dirname, '..', 'client'),
exclude: ['/api*']
})
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
加えて、APIのエンドポイントが /api/hello
となるように main.ts
と app.controller.ts
も微修正する。


5. プロジェクト間依存関係の設定
サーバーサイドがクライアントサイドのビルド生成物を配信するということは、サーバーサイドからクライアントサイドへのビルド順番の依存関係があるということだ。このプロジェクト間の依存関係は nx.json
で定義できる。

nx dep-graph
コマンドで依存関係を可視化すると以下のようになる。

この状態になっていると、サーバーサイドをビルドする際に自動的にクライアントサイドのビルドを先に実行することができる。
$ nx build api --with-deps

これでmonorepo内の依存関係を解決した上で dist
ディレクトリに生成物を出力できるようになった。ためしに dist/apps/server/main.js
を実行すれば、AngularアプリケーションがNest.jsのAPIを実行するアプリケーションが起動できる。
6. Dockerfileの作成
Dockerfileを作ってデプロイ可能なイメージをビルドするが、その前にいくつか準備が必要になる。
まず、今のバージョン(Nx v9.2.3)の angular-nest
で生成される package.json
から、postinstall
の ngcc
の実行スクリプトを削除する。これがあるとDockerfile内でモジュールをインストールしたときに不要な処理が実行される。

次に、サーバーの main.ts
が使用する環境変数を port
から PORT
に変更する。どちらでもよいが一般的に大文字のほうを使うため変更した。

そしてDockerfileは次のような形にした。Nxのビルドはサーバーサイドもバンドルするため、本当はDockerfileでは node_modules
のインストールをしたくないのだが、Nest.jsが tslib
が見つからずエラーになってしまうため仕方なくインストールしている。
FROM node:12WORKDIR /appCOPY package.json yarn.lock ./RUN yarn install --production# add appCOPY ./dist/ ./# start appCMD node apps/server/main.js
あとは docker build .
でビルドし、 docker run
コマンドで実行できることを確認する。

これでCloud Runにデプロイ可能な状態となった。
課題
- Dockerfileで node_modulesをインストールするなら、最初からNest.jsのビルドでバンドルをしないようにしたほうがイメージサイズが減るのではないか
- Nest.jsである必要もないので面倒そうならexpressに換装してもよさそう