Marginalia

Nx: Angular+Nest.jsアプリをDockerビルドする

本稿では 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/clientdist/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.tsapp.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 から、postinstallngcc の実行スクリプトを削除する。これがあると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に換装してもよさそう