Nuxt.jsをFirebaseでSSRするための開発環境をDockerで作る

# はじめに
Nuxt.jsをFirebaseへデプロイするための開発環境を整えたDockerイメージを作成します。
ちなみに[こちら](https://qiita.com/iida_teco/items/a00917cea86624193fc5)と[こちら](https://qiita.com/iida_teco/private/0e73740d4a08bfcb58f5)の記事は、今回の開発環境を作るための前段階になります！

作業環境と作成するDockerイメージの内容は以下になります。

#### 作業環境
* Windows10 pro
* Docker for Windows

#### 作成するDockerイメージの内容
* Alpine Linux
* nvm : v0.33.11
* Node.js : v6.11.5、v8.11.1
* Package Manager : yarn
* Node Modules : create-nuxt-app、firebase-tools

# Nuxt.jsの開発環境の構築

***(2019-02-21 追記)
`create-nuxt-app@2.4.0`では`v8`以降でないとインストールできなくなったようです。
そのため、下記のようにバージョンを分けることで`@google-cloud/functions-emulator`をインストールできなくなってしまってしまいました。
もし、ご存知の方がおられましたらご教授いただきたいです！***

まずは、Node.jsの`v6.11.5`と`v8.11.1`をインストールします。
なぜ２つもインストールするかというと`Nuxt.js`の`v2`では、Node.jsの`v8`以降が必要になりますが、ローカルで`Firebase Functions`の動作確認をするために必要なモジュール`@google-cloud/functions-emulator`が`v6`以前にしか対応していないためです。
そのため上記モジュールに依存している`firebase-tools`は`v6.11.5`を使ってインストールすることになります。
ちなみに`v6.11.5`と`v8.11.1`は`Cloud Functions`が公式に対応しているとされているバージョンになります。
https://cloud.google.com/functions/docs/writing/
https://cloud.google.com/functions/docs/concepts/nodejs-8-runtime

### Dockerfileの作成
さて、今回は最初からDockerfileの作成から入ります。
なぜかというと[こちらの記事](https://qiita.com/iida_teco/private/0e73740d4a08bfcb58f5)の手順でNode.jsを入れていると時間がかかりすぎるからです・・・
なので、あらかじめビルドされた各バージョンのNode.jsをコピーしてくる方法をとることにします。
それには「[Dockerのマルチステージビルド](https://docs.docker.com/develop/develop-images/multistage-build/)」という機能を使います。
まずはnvmをインストールしたイメージへNode.jsの`v6.11.5`と`v8.11.1`をインストールしたイメージからビルドしたファイルだけをコピーしてきます。

```dockerfile
FROM iidateco/alpine-nvm-node:v6.11.5 as node6
FROM iidateco/alpine-nvm-node:v8.11.1 as node8
FROM iidateco/alpine-nvm

COPY --from=node6 /root/.nvm/versions/node/v6.11.5 /root/.nvm/versions/node/v6.11.5
COPY --from=node8 /root/.nvm/versions/node/v8.11.1 /root/.nvm/versions/node/v8.11.1
```

ローカルに`alpine-nvm`や`alpine-nvm-node:v6.11.5`がある場合は、`iidateco/`を削除しても大丈夫です。
とりあえずいったんこれでビルドしてみましょう。

```console
PS C:\works\docker\alpine-nuxt-firebase> docker build -t alpine-nuxt-firebase .
```

イメージからビルドされたNode.jsをコピーしてくるだけなのでそれほど時間はかからないはず・・・。

```console
PS C:\works\docker\alpine-nuxt-firebase> docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
alpine-nuxt-firebase       latest              3437acd9ced7        10 seconds ago      310MB
iidateco/alpine-nvm-node   v8.11.1             f675583791a2        35 minutes ago      266MB
iidateco/alpine-nvm-node   v6.11.5             6ba8a1f65a76        2 hours ago         252MB
iidateco/alpine-nvm        latest              a2f9534c5a8e        4 hours ago         208MB
```

イメージからコンテナを起動してちゃんとコピーできているかを確認します。

```console
PS C:\works\docker\alpine-nuxt-firebase> docker run --rm -it alpine-nuxt-firebase bash
bash-4.4# nvm ls
        v6.11.5
        v8.11.1
node -> stable (-> v8.11.1) (default)
stable -> 8.11 (-> v8.11.1) (default)
iojs -> N/A (default)
```

コピーしただけではデフォルトのNode.jsのバージョンが設定されていないので設定します。
`Nuxt.js`の`v2`で開発したいのでデフォルトは`v8.11.1`にしておきます。

```console
bash-4.4# nvm alias default 8.11.1
default -> 8.11.1 (-> v8.11.1)
```

~~ただし、`firebase-tools`は`v6.11.5`を使ってインストールするので、以下のようにします。~~
また、パッケージマネージャは`yarn`を使いたいので`apk add`でインストールします。

```console
bash-4.4# apk update
bash-4.4# apk add --no-cache yarn
bash-4.4# yarn global add create-nuxt-app firebase-tools
```
***(`create-nuxt-app@2.4.0`が`v8`以降でないとインストールできなくなったので`nvm use 6.11.5`の記述を削除しました。)***

適当なディレクトリで動作確認します。
実行前にNode.jsを`v8.11.1`に変えておきます。

```console
bash-4.4# nvm use 8.11.1
bash-4.4# cd /tmp
bash-4.4# create-nuxt-app sample
> Generating Nuxt.js project in /tmp/sample
? Project name sample
? Project description My lovely Nuxt.js project
? Use a custom server framework none
? Use a custom UI framework none
? Choose rendering mode Universal
? Use axios module no
? Use eslint no
? Use prettier no
? Author name
? Choose a package manager yarn
```

ちゃんと動きました！
最後のパッケージマネージャだけ`yarn`にしてあとはデフォルトにしてあります。
試しに動かしてみます。

```bash
bash-4.4# cd sample/
bash-4.4# yarn dev
```

ローカルサーバが`http://localhost:3000`に立ち上がりますが、ブラウザからアクセス出来ないと思います。
なぜならホストOSとゲストOSのポートの紐付けができていないからです。
またゲストOSの環境変数の設定も必要になります。
以下のコマンドで先ほど作ったイメージを起動し直してみます。

```powershell
PS C:\works\docker\alpine-nuxt-firebase> docker run --rm -it -e HOST=0.0.0.0 -p 3000:3000 alpine-nuxt-firebase bash
```

`-e`オプションで環境変数、`-p`オプションでポートの紐付けができます。
これで立ち上がったコンソールで先ほどの手順をもう一度行ってみましょう。
コンソールには`http://127.0.0.1:3000`と表示されますが、`http://localhost:3000`をブラウザで表示してください。
今度はちゃんと表示されたかと思います！

では上記の手順をDockerfileに追記しましょう。

```dockerfile
FROM iidateco/alpine-nvm-node:v6.11.5 as node6
FROM iidateco/alpine-nvm-node:v8.11.1 as node8
FROM iidateco/alpine-nvm

COPY --from=node6 /root/.nvm/versions/node/v6.11.5 /root/.nvm/versions/node/v6.11.5
COPY --from=node8 /root/.nvm/versions/node/v8.11.1 /root/.nvm/versions/node/v8.11.1

SHELL ["/bin/bash", "-c"]

RUN \
    source ~/.bashrc && \
    apk update && \
    apk add --no-cache yarn && \
    nvm alias default 8.11.1 && \
    nvm use 6.11.5 && \
    yarn global add create-nuxt-app firebase-tools && \
    yarn cache clean

ENV HOST 0.0.0.0
```

環境変数の指定はDockerfileでできますが、ポートの紐付けはDockerfileではできないので、引き続き起動コマンドで指定するようにします。
上記のファイルで再度イメージをビルドします。
Dockerfileで環境変数を指定したので起動のコマンドは以下のようになります。

```console
PS C:\works\docker\alpine-nuxt-firebase> docker run --rm -it -p 3000:3000 alpine-nuxt-firebase bash
```

Nuxt.jsの動作確認をしてちゃんと動いたら成功です！

# ホストOSのディレクトリのマウント
さて、これからFirebaseでSSRするための設定をしていくのですが、いままで使っていた起動コマンドでは作業がやりにくいので、以下のようにコマンドを変えます。

```console
PS C:\works\project> docker run -it --mount type=bind,src=$(pwd)/,dst=/project -p 3000:3000 -p 5000:5000 -p 5001:5001 alpine-nuxt-firebase bash
```

上記コマンドをプロジェクトを作成したいディレクトリまでコンソールで移動してから実行してください。
いままで`--rm`オプションをつけていましたが、これだとDockerコンテナを終了したときにコンテナが削除されてしまい、作業途中のデータが無くなってしまうのでなくしました。
`--mount type=bind,src=$(pwd)/,dst=/project`のオプションで、コマンド実行ディレクトリが`/project`にマウントされるようになります。
では、起動したコンテナのコンソールで`/project`に移動して、`create-nuxt-app`でプロジェクトを作成してみましょう。
ホストOSのマウントしたディレクトリにもファイルが作成されていると思います。

# FirebaseでSSRするための設定
Dockerの環境はこれで準備ができたので、Nuxt.jsをFirebaseでSSRするための設定をしていきます。
こちらにサンプルを用意しましたので、それを参照しながら要点を説明していきます。
https://github.com/iida-teco/nuxt-firebase-ssr-template.git


### ディレクトリ構成
Nuxt.jsをFirebaseでSSRするためには`Hosting`にアップロードするものと`Funcstions`にアップロードするものに分けることになります。
`app`というディレクトリがNuxt.jsのファイルがおいてあります。
`functions`というディレクトリには`Functions`で使われるファイルがおいてあります。
以下のようなディレクトリ構成になっていると思います。

```console
bash-4.4# ls -la
total 226
drwxrwxrwx 2 root root   4096 Dec  6 02:27 .
drwxrwxrwx 2 root root      0 Dec  6 02:23 ..
-rwxr-xr-x 1 root root    207 Dec  3 08:20 .editorconfig
drwxrwxrwx 2 root root      0 Dec  6 02:27 .git
-rwxr-xr-x 1 root root   1214 Dec  6 08:03 .gitignore
-rwxr-xr-x 1 root root    291 Dec  6 02:29 README.md
drwxrwxrwx 2 root root      0 Dec  3 08:43 app
-rwxr-xr-x 1 root root    272 Dec  6 02:21 firebase.json
drwxrwxrwx 2 root root      0 Dec  3 09:55 functions
-rwxr-xr-x 1 root root    889 Dec  6 02:19 nuxt.config.js
-rwxr-xr-x 1 root root    666 Dec  6 08:04 package.json
-rwxr-xr-x 1 root root 210503 Dec  3 08:22 yarn.lock
bash-4.4# ls -la app/
total 4
drwxrwxrwx 2 root root    0 Dec  3 08:43 .
drwxrwxrwx 2 root root 4096 Dec  6 02:27 ..
drwxrwxrwx 2 root root    0 Dec  3 08:20 assets
drwxrwxrwx 2 root root    0 Dec  3 08:20 components
drwxrwxrwx 2 root root    0 Dec  3 08:20 layouts
drwxrwxrwx 2 root root    0 Dec  3 08:20 middleware
drwxrwxrwx 2 root root    0 Dec  3 08:20 pages
drwxrwxrwx 2 root root    0 Dec  3 08:20 plugins
drwxrwxrwx 2 root root    0 Dec  3 08:20 static
drwxrwxrwx 2 root root    0 Dec  3 08:20 store
bash-4.4#
bash-4.4# ls -la functions/
total 6
drwxrwxrwx 2 root root    0 Dec  3 09:55 .
drwxrwxrwx 2 root root 4096 Dec  6 02:27 ..
-rwxr-xr-x 1 root root  619 Dec  6 02:20 index.js
-rwxr-xr-x 1 root root  496 Dec  6 02:22 package.json
```

### ビルド設定の変更
`nuxt.config.js`で設定を行います。
変更箇所を抜粋して説明します。

```javascript:nuxt.config.js
module.exports = {
  mode: 'universal',
  srcDir: 'app',
  buildDir: 'functions/nuxt',
}
```

`srcDir`にNuxt.jsのファイルがおいてある場所を指定します。
`buildDir`にビルドした結果を出力する先を指定します。
`Cloud Functions`でビルドした結果のファイルを参照するため`functions`以下に出力します。

```javascript:nuxt.config.js
  /*
  ** Build configuration
  */
  build: {
    publicPath: '/assets/',
    /*
    ** You can extend webpack config here
    */
    extend(config, ctx) {
    }
  }
}
```

`publicPath`にはCDNのURLを記述するようなのですが、ビルドしたファイルを`hosting`しているディレクトリ（後述）を指定します。

ビルド後のファイルの配置を`package.json`で設定します。

```json:package.json
  "scripts": {
    "setup": "yarn && cd functions && yarn && cd ..",
    "dev": "nuxt dev",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "build": "nuxt build && yarn build:copy",
    "build:copy": "rimraf public/* && cpx \"functions/nuxt/dist/**/*\" public/assets/ && cpx \"app/static/*\" public/",
    "serve": "firebase serve --host 0.0.0.0",
    "deploy": "firebase deploy"
  },
```
`build:copy`でビルドされたファイルを`public`にコピーしています。
このディレクトリがFirebaseに`hosting`されます。

### Firebaseの設定
`firebase.json`で設定します。

```json:firebase.json
{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "function": "ssr"
      }
    ]
  },
  "functions": {
    "source": "functions"
  }
}
```
`public`には`hosting`するディレクトリを指定します。
`rewrites`ですべてのアクセスを`ssr`ファンクションへリダイレクトするようにしておきます。
`functions`には`Cloud Functions`で動かすディレクトリを指定します。

### SSRするファンクションの説明
`functions/index.js`がSSRするファンクションになります。

```javascript:functions/index.js
const functions = require('firebase-functions');
const express = require('express');
const { Nuxt } = require('nuxt');

const app = express();
const nuxt = new Nuxt({
    dev: false,
    buildDir: 'nuxt',
    build: {
        publicPath: '/assets/'
    }
});

function handleRequest(req, res) {
    res.set('Cache-Control', 'public, max-age=600, s-maxage=1200');
    return new Promise((resolve, reject) => {
        nuxt.render(req, res, (promise) => {
            promise.then(resolve).catch(reject);
        });
    });
}

app.use(handleRequest);
exports.ssr = functions.https.onRequest(app);
```
`express`を使って、Nuxt.jsがレンダリングした結果を表示します。
Nuxt.jsを初期化する祭に指定するオプションは`nuxt.config.js`と同じ内容にします。
`buildDir`だけは階層が違うので`nuxt`だけにしてあります。

また、`functions/package.json`に以下の設定を入れておきます。

```json:functions/package.json
  "engines": {
    "node": "8"
  },
```

これはNode.jsを`v8`で動かすという設定になります。

以上がNuxt.jsをFirebaseでSSRするための変更の要点になります。

### Firebaseのローカル環境で実行
実行する前にFirebaseへのログインとプロジェクトの紐付けをします。
Firebaseのアカウントを作成してFirebaseコンソールで適当にプロジェクトを作成してください。

まずはFirebaseにログインします。
ゲストOSのコンソールでのログインは、ブラウザに連携できないのでURL経由でのログインになります。

```console
bash-4.4# firebase login --no-localhost
```

上記コマンドで表示されるURLをブラウザで開くとアカウントの選択と権限の許可リクエストが表示されるので適宜選択すると、認証コードが表示されます。
その認証コードをコンソールに入力すればログイン完了です。

ログインできたら作ったプロジェクトを紐付けします。

```console
bash-4.4# firebase use --add
```

上記コマンドを入力するとプロジェクトの選択肢が表示されるので、作ったプロジェクトを選択します。
次の`alias`の指定は適当に`default`でも指定してください。

これで準備ができたので実行してみます！
`package.json`の`script`に`serve`コマンドを用意しておいたので、以下を実行すれば開始されます。
`firebase serve`に`--host 0.0.0.0`オプションをつけないとホストOSのブラウザからアクセスできませんでした。

```console
bash-4.4# yarn setup
bash-4.4# yarn build
bash-4.4# yarn serve
```

初回だと`yarn setup`に結構時間がかかると思います。

```console
bash-4.4# yarn serve
yarn run v1.7.0
$ firebase serve --host 0.0.0.0

=== Serving from '/project/nuxt-firebase-ssr-template'...

i  functions: Preparing to emulate functions.
Warning: You're using Node.js v8.11.1 but Google Cloud Functions only supports v6.11.5.
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://0.0.0.0:5000
✔  functions: ssr: http://0.0.0.0:5001/fana-dev/us-central1/ssr
```
上記のように表示されたら起動完了なのですが、`http://0.0.0.0:5000`ではなく`http://localhost:5000`をブラウザで開くと表示が確認できると思います。

これでNuxt.jsをFirebaseでSSRできるようになりました！
あとは、`yarn deploy`でFirebaseにデプロイして動作を確認してみてください！

### ハマった箇所
今回使用したサンプルでは出てきませんでしたが、環境を作っていてハマった箇所を共有しておきます。

- `package.json`の`dependencies`にあるモジュールは`functions/package.json`の`dependencies`にも書いておく必要がある

これ、ローカル環境では動くのですが、Firebaseにデプロイすると動かなくなるので、原因が特定できなくて苦労しました。

- Docker for Windowsではホットリロードが機能しない

ホストOSでファイルを書き換えても自動でリロードしてくれません。
`nuxt.config.js`に以下を記述することで対応できます。

```javascript:nuxt.config.js
  watchers: {
    webpack: {
      poll: true
    }
  },
```

# まとめ
Dockerの環境構築は初めてだったので手探りでやってきました。
今回のDockerの構成も、もっとスマートにできるかもしれません。
これまでの記事でDockerでの開発環境の構築方法などの手助けになれば幸いです！

FirebaseとNuxt.jsも今回初めて触りましたが、いろいろ気が利いてて使いやすいサービスとフレームワークだと思いました。
次はこの構成でなにか動くものを作りたいと思います！

## 参考にしたサイト
[Use multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/)
[Dockerのマルチステージビルドを使う](https://qiita.com/carimatics/items/01663d32bf9983cfbcfe)
[Container networking](https://docs.docker.com/config/containers/container-networking/)
[nuxt.js + firebase (cloud functions) で最小構成SSR](https://marunouchi-tech.i-studio.co.jp/5237/)
[Create a SSR + Serverless App with Firebase & Nuxt.js within an hour](https://itnext.io/how-to-create-a-ssr-serverless-app-with-firebase-nuxt-js-in-an-hour-6e6e03d0b3b8)
[IT研修でVuePress+Express+Nuxt on Dockerでシステムを作成した話](http://kurowasi2525.hatenablog.com/entry/2018/04/29/201924)
[Hot reloading doesn't work in docker container on Windows](https://github.com/nuxt/nuxt.js/issues/2481)