NowでNuxtとPuppeteerをDockerでデプロイする

もうやりたくねー！！です！！

解説専用ではありませんが作っていたコードはGitHubにあります！

[khsk/togetter-to-text: TogetterをPuppeteerでスクレイピングする](https://github.com/khsk/togetter-to-text)

# どんなものを作ったか

フロントをNuxt.jsで構築し、APIでPuppeteerを起動しスクレイピング結果を返して表示する。

# 動作環境

* Docker mhart/alpine-node:base-10
* Nuxt 2.2.0
* Puppeteer 1.9.0
* express 4.16.4


# NuxtのserverMiddlewareでPuppeteer動かない問題

詳細はこちら
[Nuxt.jsからPuppeteerが実行できない TypeError: Cannot read property 'on' of undefined - Qiita](https://qiita.com/khsk/items/ed40f36f78a9498baee7)

NuxtにAPIサーバーを追加する場合、本来ならNuxt単体でHTTPリクエストを処理できるのですが、Puppeteerが動作しなかったため、expressにNuxtを追加する形になりました。
しんどい！

# nowでPuppeteer動かない問題

[Google's Puppeteer doesn't work in now.sh · Issue #861 · zeit/now-cli](https://github.com/zeit/now-cli/issues/861)

こちらは知識としては既知だったのですが、nowでNPM形式ではPuppeteerが動きません。
せっかくPuppeteerがchromiumを同梱してくれるようになったのに…

動かすにはDockerfileをデプロイする必要があります。


# nowでPuppeteer入りDockerがデプロイできない問題

Puppeteerを動かすDockerfileについては
[zeit/now-examples: Examples of Now deployments you can use](https://github.com/zeit/now-examples)
の
[now-examples/misc-screenshot at master · zeit/now-examples](https://github.com/zeit/now-examples/tree/master/misc-screenshot)
が役立ちます。

というか私がDocker及びサーバー構築に無知なのでこれをそのまま動かそうとしました。

が、Puppeteerの他にNuxtなどを含めた結果、ファイルサイズが100MBを超え、サイズ制限に引っかかってデプロイできませんでした。

## ローカルのnode_modulesのデプロイに気をつけよう

参照元の構成ではメインのコードが`./src`ディレクトリに集約されています。

私のコードはルート直下にすべてあることと、

* [Dockerのマルチステージビルドを使う - Qiita](https://qiita.com/carimatics/items/01663d32bf9983cfbcfe)
* [Docker multi stage buildで変わるDockerfileの常識 - Qiita](https://qiita.com/minamijoyo/items/711704e85b45ff5d6405)

マルチステージビルドへの理解が薄いため、

```docker
FROM mhart/alpine-node:10 as build
WORKDIR /usr/src
#COPY package.json package-lock.json /usr/src/
COPY . /usr/src/
```

`.`をまるごとコピーしていました。
これではせっかくビルド環境で

```docker
RUN PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 yarn install --production
```

しているのに、ローカルで動かすdevDependenciesやchromiumが含まれてしまってファイルサイズが膨張してしまっていました。
(cpにexceptできれば簡単でしたが)

そのため、ローカルファイルを全部コピーした後、

```docker
RUN rm -rf ./node_modules
RUN PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 yarn install --production
```

とローカルの`node_modules`を削除してから改めてビルドしました。

…が、それでも結果は180MB → 110MBとデプロイには至りませんでした…

(つい最近の更新では`.nowignore`が追加されたようです)

# Dockerデプロイをv1で動かそう

サイズ制限の問題は
[Image size limit of 100 Mb · Issue #1523 · zeit/now-cli](https://github.com/zeit/now-cli/issues/1523)
などで話されています。

ここでは
`now.json`
を

```json
"features": {
  "cloud": "v1"
},
```

と、普段`v2`で動くモードを`v1`に変更すれば動くよと言われています。

`v2`と`v1`の違いは正直よくわかりません…

* [now.sh (zeit) の docker デプロイ - Qiita](https://qiita.com/high-u/items/4ec6991a997a197d39ce)
* [ZEIT – Serverless Docker Beta](https://zeit.co/blog/serverless-docker)
* [Deploying Docker Apps - ZEIT Documentation](https://zeit.co/docs/deployment-types/docker)

が、確かにDockerのデプロイ自体には成功しました。


# デプロイに成功したのにアクセスがタイムアウトする

Docker内でexpressが正常に動いているかがわかりませんでした。
しかしログ的には起動自体は完了していそうです。
しかししかし、動かすJavaScriptを
[now-examples/node-express at master · zeit/now-examples](https://github.com/zeit/now-examples/tree/master/node-express)
の簡単なexpressにしても同様…
デプロイ先に`3000`番ポートでアクセスしてもダメでした。

ここで、個人利用のDockerってたしかポートフォワーディングしないとダメだったような記憶があったことを思い出しました。

また、npmのデプロイでもnowは暗黙的にスクリプトで開けたポートをデプロイ先の`80`番ポートとして扱ってくれていることを思い出しました。

そこで、Dockerfileに

```docker
EXPOSE 3000
```

を追加することで`80`番ポートでアクセスするとレスポンスが返ってくることが確認できました。

なぜそうなるかと言うと(なぜ`EXPOSE 3000`がなかったかというと)

* [ZEIT – Serverless Docker Beta](https://zeit.co/blog/serverless-docker)

>Automatic port discovery. We no longer rely on the '`EXPOSE`' instruction. We automatically forward traffic to the port of the process started by '`CMD`'

`v2`からは自動的に処理してくれるので、`v1`で動かそうとすると手動設定が必要だったわけです。
ドキュメントをちゃんと読むことも大切ですが、過去にDockerを触ってなかったら非常に危なかった…

(でもEXPOSEしても`-p`オプションを使わないとうまくいかないのでは？とすごく悩んではいました。今でもちゃんとした動きは把握していないデス)

# レスポンス真っ暗問題

ちゃんとポートフォワーディングできるようになりましたが、レスポンスがなくてまっさら。
ただ、スクリプトを
[now-examples/node-express at master · zeit/now-examples](https://github.com/zeit/now-examples/tree/master/node-express)
に変えるとちゃんと表示されたので、expressの問題だろうと見当はつきました。

結論から言うと、expressでNuxtを動かすためにコピペしたコード、

[【Node.js】express + Nuxt.jsでシンプルなテンプレートを作った - Qiita](https://qiita.com/seapolis/items/118aec7aa41ac82a6b46)
の

```js
const express = require("express")
const { Nuxt, Builder } = require("nuxt")

const app = express()

let config = require("./nuxt.config.js")
config.dev = !(process.env.NODE_ENV === "production")
const nuxt = new Nuxt(config)

if (config.dev) {
    const builder = new Builder(nuxt)
    builder.build()
}
```

が問題でした。

こちらもコピペしたDockerfileでは

```docker
FROM mhart/alpine-node:base-10
WORKDIR /usr/src
ENV NODE_ENV="production"
```

としているので、production環境になりNuxtのための`builder`が働いていませんでした。

expressでNuxtを動かす背景を理解していないために起きた問題ですね。

```js
// if (config.dev) {
    const builder = new Builder(nuxt)
    builder.build()
// }
```

とすることで解決。

# Puppeteer動かない問題

ここからはPuppeteerとの戦いになります…

## launchできない

これはローカルのコードでは手元のchromiumを動かそうとしますが、Dockerではちゃんと用意したものを使わないといけないからです。
ここで

```js
config.dev = !(process.env.NODE_ENV === "production")
```

`NODE_ENV === "production"`が役立ち、デプロイ用の記述を使い分けられることに気づいたのが良かったです。

[now-examples/setup.js at master · zeit/now-examples](https://github.com/zeit/now-examples/blob/master/misc-screenshot/src/setup.js#L23-L28)

をちゃんと確認して、

```js
        if (process.env.NODE_ENV === 'production') {
            this.browser = await puppeteer.launch({
                executablePath: '/usr/bin/chromium-browser',
                args: ['--no-sandbox', '--headless', '--disable-gpu', '-—disable-dev-tools'],
                dumpio: true,
                devtools: false,
            })
        } else {
            this.browser = await puppeteer.launch({ headless: true, })
        }
```

productionではalpineのchromiumのパスをちゃんと指定しましょう。

## `.click()`に失敗する

```js
if (await this.page.$(selector)) {
    await this.page.click(selector)
}
```

ちゃんと要素の存在を確認しているのに、デプロイ先では

`Node is either not visible or not an` (HTMLElement)
と出てきてクリックに失敗します。

[Node is either not visible or not an HTMLElement for page.click() in Puppeteer 1.6 · Issue #2977 · GoogleChrome/puppeteer](https://github.com/GoogleChrome/puppeteer/issues/2977)

不本意ですが、倣って`evaluate`でブラウザ内でクリックすることになりました。


```js:本番はクリック結果を待つためにPromise.allを使います
        const moreTweetId = '#more_tweet_btn'
        if (await this.page.$(moreTweetId)) {
            if (process.env.NODE_ENV === 'production') {
                // now環境で.click()にError: Node is either not visible or not an HTMLElementがでるのでeval clickで回避する
                await Promise.all([this.page.waitFor((moreTweetId) => !document.querySelector(moreTweetId), moreTweetId), this.page.evaluate((moreTweetId) => { document.querySelector(moreTweetId).click() }, moreTweetId),])
            } else {
                await Promise.all([this.page.waitFor((moreTweetId) => !document.querySelector(moreTweetId), moreTweetId), this.page.click(moreTweetId),])
            }
        }
```

# デプロイ制限

nowはデプロイ無制限をうたっていますが、

[logs prematurely rate limited · Issue #1169 · zeit/now-cli](https://github.com/zeit/now-cli/issues/1169)

何回も(それでも結構な回数)デプロイした結果、制限に引っかかってしまいました。
動かない焦りで雑なデプロイを繰り返していたので、逆に良かったかも…


# その他 不定期失敗

## yarn install

デプロイ時のyarn installで

```
error https://registry.yarnpkg.com/@nuxtjs/youch/-/youch-4.2.3.tgz: Extracting tar content of undefined failed, the file appears to be corrupt: "Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?"
```

が出るときがありました。時間を空けて解消。

## CDN

Puppeteer動作時に`page.goto`が
`net::ERR_NAME_RESOLUTION_FAILED`
になることがありました。
デプロイし直すと直ったり直らなかったりします…