NixでNext.jsアプリをビルドする
Tags: nextjs, nix
Published: 2020-08-14 13:09:54

この記事では，[Nix](https://nixos.org/)を使って[Next.js](https://nextjs.org/)アプリをビルドします．[GitHub](https://github.com/pandaman64/build-next-with-nix)にこの記事の内容を実行したリポジトリを用意したのでそちらもご覧ください．
Nixを使う利点は適当にググってください．

まずはプロジェクトディレクトリを作成してgitリポジトリを初期化しましょう．

```bash
$ mkdir build-next-with-nix
$ cd build-next-with-nix/
$ git init
Initialized empty Git repository in /path/to/build-next-with-nix/.git/
```

つぎに，ビルドに必要なツールをインストールします．このプロジェクトでは以下のツールを使います．
- [gitignore.nix](https://github.com/hercules-ci/gitignore.nix): .gitignoreに応じて必要なファイルをフィルタします．
- [nix-npm-buildpackage](https://github.com/serokell/nix-npm-buildpackage): npm(yarn)プロジェクトのビルドに使います．

[niv](https://github.com/nmattia/niv)を使ってインストールします．さくっと入れれてハッシュも自動で計算してくれるのはすごい！

```bash
$ niv init
Initializing
  Creating nix/sources.nix
  Creating nix/sources.json
  Importing 'niv' ...
  Adding package niv
    Writing new sources file
  Done: Adding package niv
  Importing 'nixpkgs' ...
  Adding package nixpkgs
    Writing new sources file
  Done: Adding package nixpkgs
Done: Initializing
$ niv add hercules-ci/gitignore.nix
Adding package gitignore.nix
  Writing new sources file
Done: Adding package gitignore.nix
$ niv add serokell/nix-npm-buildpackage
Adding package nix-npm-buildpackage
  Writing new sources file
Done: Adding package nix-npm-buildpackage
```

それではNextアプリを作っていきましょう．今回は[TypeScript + ESLint + Jestのテンプレート](https://github.com/vercel/next.js/tree/master/examples/with-typescript-eslint-jest)を使うことにします．
まずは`shell.nix`を作って`npm`のインストールされた開発環境を立ち上げます．
```nix
# shell.nix
{ sources ? import ./nix/sources.nix }:
let
  pkgs = import sources.nixpkgs {};
in
  pkgs.mkShell {
    buildInputs = [
      pkgs.nodejs
    ];
  }
```

```bash
$ nix-shell
[nix-shell:~/build-next-with-nix]$
```

テンプレートを展開しましょう．これには少し時間がかかります．

```bash
$ npx create-next-app next-app --example https://github.com/vercel/next.js/tree/master/examples/with-typescript-eslint-jest
```

それでは，サーバを起動してみましょう！
```bash
$ cd next-app/
$ npm run dev

> with-typescript-eslint-jest@1.0.0 dev /home/pan/build-next-with-nix/next-app
> next dev

ready - started server on http://localhost:3000
event - compiled successfully
```

うまくいきましたね．
つぎに，このプロジェクトをNixを使ってビルドします．
まず，`next.config.js`を使ってNextの設定を変更する必要があります．
```js
// next.config.js
module.exports = {
  distDir: 'output',
  generateBuildId: async () => {
    return process.env.NEXT_BUILD_ID || 'next-build'
  },
}
```
ひとつ目の設定はNextの出力ディレクトリを変更しています．これは.gitignoreに含まれないディレクトリを指定しなければいけません[^1]．
次の設定はビルドを決定的にするために必要です．NextはデフォルトでビルドIDをランダムに生成するため，固定値に上書きしてあげる必要があります．ここではNixから環境変数経由で与えることにします．

[^1]: なぜかは分かりません．

次のdefault.nixをプロジェクトルートに作成してください．
```nix
# default.nix
{ sources ? import ./nix/sources.nix }:
let
  pkgs = import sources.nixpkgs {};
  bp = pkgs.callPackage sources.nix-npm-buildpackage {};
  inherit (import sources."gitignore.nix" { inherit (pkgs) lib; }) gitignoreSource;
  src = gitignoreSource ./next-app;
in
  bp.buildNpmPackage {
    inherit src;
    npmBuild = "npm run build";
    extraEnvVars = {
      NEXT_NIX_BUILD = "true";
      NEXT_BUILD_ID = builtins.hashString "sha256" "${src}";
    };
  }
```

ビルドしてみましょう！
```bash
$ nix-build
```

成功したら，`result`フォルダからサーバを起動してみましょう．

```bash
$ cd result
$ ./bin/npm run start

> with-typescript-eslint-jest@1.0.0 start /nix/store/wmw2hld9cdfxy7kdggb4qaxk7ravl8a4-with-typescript-eslint-jest-1.0.0
> next start

ready - started server on http://localhost:3000
```

😁

Dockerイメージを作ることもできます．`Dockerfile.nix`を次の内容で作成します．
```nix
# Dockerfile.nix
{ sources ? import ./nix/sources.nix }:
let
  pkgs = import sources.nixpkgs {};
  contents = import ./default.nix { inherit sources; };
in
  pkgs.dockerTools.buildImage {
    name = "build-next-with-nix";
    tag = "latest";
    inherit contents;
    config = {
      Env = [ "NODE_ENV=production" ];
      Cmd = [ "/bin/npm" "run" "start" "--scripts-prepend-node-path" ];
    };
  }
```

ビルドしてみましょう．
```bash
$ nix-build Dockerfile.nix
```
作成したDockerイメージは`docker load`で読み込めます．
```bash
$ docker load -i result 
Loaded image: build-next-with-nix:latest
```
実行してみましょう！
```bash
$ docker run -p 3000:3000 build-next-with-nix

> with-typescript-eslint-jest@1.0.0 start /nix/store/231hhb2sbpqvcg1y4jsmhzycfwnhnvbn-with-typescript-eslint-jest-1.0.0
> next start

ready - started server on http://localhost:3000
```

😎👍