docker-compose+puppeteerですぐに開発できるイメージを構築【日本語対応】


2019.10.24 更新

詳しい原因がまだわかっていないのですが、 alpine:edge を指定していると正常にChromiumが立ち上がらない状態になっていることを確認したため一時的に alpine:latest を指定するように記事を修正しました。 この変更により日本語フォントをシンプルにインストールできなくなっています(タイトル詐欺ですみません)。

puppeteerのREADMEが更新されていたので記事内のバージョンも1.17.0から1.19.0に更新しました。


  • Dockerfile / docker-compose.yml は最小限自分で書く・引用する場合は引用元を明確にし、最新の情報へアップグレードできるようにする
  • ローカル環境を汚したりせずコンテナに入って webpack --watch しながら node ですぐ動作確認したい
    • クラウドサービスへデプロイできるようにもしたい

※Dockerの知識は2018年末ぐらいで止まっているので一部古い情報の可能性があります

(最終的な)ディレクトリ構成

  • app ソースコードなどビルドに必要なファイルのディレクトリ
    • dist ビルド済みファイル
    • src ソースコード
      • index.ts
    • package.json
    • tsconfig.json
    • webpack.config.js
    • yarn.lock
  • shared コンテナと双方向で共有するディレクトリ、今回はスクショ撮ったらローカルで見たいので用意(用途によっては不要)
  • .dockerignore
  • docker-compose.yml
  • Dockerfile

まずはDockerfile

手短な方法を探しましたが公式的にはおまじないが必要そうです。トラブルシューティングにある Alpine を利用しました。

################################################################################
# https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
################################################################################
# FROM alpine:edge # 2019/10/24 Chromiumが起動できなくなったので使用しない
FROM alpine:latest

# Installs latest Chromium (77) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      freetype-dev \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      yarn

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

# Puppeteer v1.19.0 works with Chromium 77.
RUN yarn add [email protected]

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

# Run everything after as non-privileged user.
USER pptruser
################################################################################

WORKDIR /app
CMD ["sh"]

### で囲まれている部分は引用で最後の数行だけオリジナルです。appをカレントディレクトリとしてshが立ち上がるようにします。
puppeteerがyarn推しなのでnpmが使いたい場合はyarnになっている部分を適宜修正してください。

/app のディレクトリ名を変える場合、上記chownしている部分も変える必要がありそうです

日本語ページをレンダリングしたい場合は下記追加してください

       ttf-freefont \
       nodejs \
-      yarn
+      yarm \
+      font-noto-cjk # (現時点では alpine:edge で使用可能)
+      unifont # (alpine:latest でも使用可能)

docker-compose.yml

version: '3'
services:
  app:
    privileged: true
    build: .
    volumes: 
      - ./shared:/shared
      - ./app:/app

privileged: true がないとうまく動きません

package.json とインストール

コンテナに入って作っていきます。よかったらこちらの記事も参照ください。

$ docker-compose build --no-cache # 初回は下記で実行されるので不要、--no-cacheはうまく反映されなくなったらつけてください
$ docker-compose run --rm app # ビルド済みコンテナに入る
...
/app $ whoami # 一応コンテナに入ったことを確認
pptruser # どーん
/app $ yarn init -y # とりあえず package.json を作成
...
/app $ yarn add --dev typescript webpack webpack-cli ts-loader @types/puppeteer
...

ソースコードを書く

tsconfig.json を作っておきます

/app $ ./node_modules/.bin/tsc --init

続いて app/src/index.ts を作ります。コード自体はありきたりなので特に触れません。

import puppeteer from 'puppeteer';

(async () => {
  try {
    const browser = await puppeteer.launch({
      executablePath: '/usr/bin/chromium-browser',
      args: ['--disable-dev-shm-usage']
    }); // オプションはトラブルシューティング参照
    const page = await browser.newPage();
    await page.goto('https://qiita.com/');
    await page.screenshot({
      path: '/shared/result.png'
    });
  } catch (e) {
    console.error(e);
  }
})();

実行できるようにする

package.json に scripts を追加します

 {
   "name": "app",
   "version": "1.0.0",
-  "main": "index.js",
+  "main": "dist/main.js",
   "license": "MIT",
+  "scripts": {
+    "build": "webpack",
+    "start": "webpack --watch --progress"
+  },
   "devDependencies": {
     "@types/puppeteer": "^1.20.1",
     "ts-loader": "^6.2.0",
     "typescript": "^3.6.3",
     "webpack": "^4.41.0",
     "webpack-cli": "^3.3.9"
   }
 }

webpack の設定がまだだったので作ります、 webpack init は色々遠回りだったので調整済みの設定を貼ります

const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'development',
  entry: './src/index.ts',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [new webpack.ProgressPlugin()],
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/,
        include: [path.resolve(__dirname, 'src')],
        loader: 'ts-loader'
      }
    ]
  },
  resolve: {
    modules: ['node_modules'],
    extensions: ['.ts', '.js']
  },
  target: 'node'
};

最後のtargetだけ指定しておかないとエラーで起動できなくなります

実行

/app $ yarn start

でビルドしつつwatchが始まります。別のシェルを起動してそちらで都度nodeを呼び出すと実行されるわけですが、dockerを使ってるとちょっとだけ面倒です。

$ docker ps # コンテナIDを確認するために表示
CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS              PORTS                    NAMES
9721b1846c11        puppeteer-tool_app          "sh"                     7 minutes ago       Up 7 minutes                                 puppeteer-tool_app_run_753be0a1576d
$ docker exec -it 9721b1846c11 /bin/sh # コンテナに入ってシェル起動
/app $ node . # 実行!

これで /dist に result.png が出来上がっているはずです。

デプロイ用イメージに切り替える

上記は開発用ですがクラウドで使用したい場合はコンテナを起動するだけで実行してほしいはずです。下記のように変更します。

Dockerfile

+ COPY app app
  WORKDIR /app
+ RUN yarn install && yarn build
- CMD ["sh"]
+ CMD ["node", "."]

docker-compose.yml

  app:
    privileged: true
    build: .
    volumes: 
      - ./shared:/shared
-      - ./app:/app

app/distapp/node_modules は不要なので .dockerignore をプロジェクトのルートディレクトリに設置しておきましょう

app/dist
app/node_modules

下記でシェルには入らず実行後終了されるはずです。

$ docker-compose build
...
$ docker-compose up

このパターンだと試行錯誤しながら開発しづらいので最終段階になったら変更するのが良さそうです。

参考にした記事

https://qiita.com/EBIHARA_kenji/items/31b7c1c62426bdabd263
https://qiita.com/sekizo/items/27cc9b406332afc674f6