2020年にDockerでFirebaseアプリの開発環境を構築してみた

この記事は「[【マイスター・ギルド】本物の Advent Calendar 2020](https://adventar.org/calendars/5454)」15日目の記事です。

---

ちょっと前ですがWindows System for Linux2（WSL2）がリリースされました。これでWindows上でLinuxを動かすことができます。
それともう一つ、WSL2にすることでHomeなWindowsでもDocker Desktop for Windowsを使うことができます。案外これは知らない人もいるかもしれません。
Homeな私はこれまでDocker ToolBoxを使ってましたが、あまり良い体験とは言えず、Dockerに対してのモチベーションがあまりありませんでした。
しかしDocker Desktop for Windowsを使ったところ、非常に使いやすかったので、DockerでFirebase Cloud FunctionsをバックエンドにしたTSアプリを開発する環境を構築してみました。フロントはVue.jsにしてみます。これはそれの結果をまとめた記事になります。

↓はこの記事で説明する環境構築に関係のあるファイルのディレクトリ構成図です。もちろんこれ以外にソースファイルやアプリケーションの設定ファイルがあります。

```:ディレクトリ構成図
.
├──app
│  ├──functions
│  │  ├──lib
│  │  ├──node_modules
│  │  ├──.dockerignore
│  │  ├──copy-node_modules.sh
│  │  ├──Dockerfile
│  │  └──package.json
│  │     
│  ├──vuejs
│  │  ├──dist
│  │  ├──node_modules
│  │  ├──.dockerignore
│  │  ├──copy-node_module.sh
│  │  ├──Dockerfile
│  │  └──vue.vonfig.js
│  │
│  └──firebase.json
│
├──swagger
│  └──index.yml
│
└──docker-compose.yml
```

Dockerコンテナ群は↓のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177194/6a4e2409-01b8-a059-f22a-29e9e1a08656.png)

ちなみに今回はたまたまDBを使わないアプリの開発でした。DBを使う場合、FirebaseのCloud Firestoreなんかを使う場合はおそらくこの構成のままですが、もしMySQLなんかが必要になる場合は`db`のコンテナが追加されると思います。

TSアプリの開発がほとんど今回初めてだったのですが、Dockerで開発環境を構築する場合、次の問題に当たりました。

- TSのビルド先をWindowsと共有するとホットリロードが効かない
- node_modulesをWindowsと共有するとビルドがめっちゃ遅い
- *.d.tsがエディタ側に無いと開発時に型の恩恵が受けられない

またFirebaseの次の仕様も頭を悩ませました。

- エミュレーターを立ち上げるとAPIに`プロジェクト名/リージョン名/`のプレフィックスが付く

では、それぞれのコンテナを見ていきます。

# firebaseコンテナ

```package.json
"scripts": {
  ...
  "emulators": "tsc-watch --onFirstSuccess \"firebase emulators:start --token ${FIREBASE_TOKEN}\""
  ...
},
...
"dependencies": {
  ...
  "firebase-admin": "^9.2.0",
  "firebase-functions": "^3.6.1",
  ...
}
...
"devDependencies": {
  ...
  "tsc-watch": "^4.2.9"
  ...
}
```

Firebase CLIの[Cloud Functions Emulators](https://firebase.google.com/docs/functions/local-emulator)を利用します。`tsc-watch`を導入してホットリロードで開発できるようにします。

```
$ npm run emulators
```
でホットリロードしつつ開発が行えるエミュレーターが立ち上がります。

```firebase.json
...
  "hosting": {
    "public": "vuejs/dist",
    ...
    "rewrites": [
      {
        "source": "/get-users",
        "function": "get-users"
      }
    ]
  },
  "emulators": {
    "functions": {
      "host": "0.0.0.0",
      "port": 5001
    },
    "hosting": {
      "host": "0.0.0.0",
      "port": 5000
    },
    "ui": {
      "enabled": true,
      "host": "0.0.0.0",
      "port": 4000
    }
  }
...
```
Firebaseの設定です。
`hosting`ではまず公開ディレクトリをVue.jsのデフォルトのビルド先に指定しています（`public`）。`rewrites`では`/～`にアクセスすることでCloud Functionsの各関数を呼び出せるようにしています。これはCloud Functions Emulatorsで用意されるAPIのエンドポイントにはプレフィックスにプロジェクト名、リージョン名が付くのですが、SwaggerからAPIを叩くときにこのプレフィックス部分を無視したいからです（Swaggerのymlファイルにプロジェクト名、リージョン名を書きたくない）。APIが増えればここに追記する必要が出てきます。
`emulators`ではDocker外からアクセスできるように`host`を指定しているのと、Cloud Functions EmulatorsのUI画面を有効にし、一番小さいポート番号が割り当たるようにしています。あまり使わないかもですが、これでDocker Desktop for Windowsの「OPEN IN BROWSER」でエミュレーターのダッシュボード的な画面が開くようになります。

```docker-compose.yml
volumes:
  firebase-node_modules:
  firebase-lib:

services:
  firebase:
    build: ./app/functions
    image: firebase
    container_name: firebase
    ports:
      - 4000:4000
      - 5000:5000
      - 5001:5001
    volumes:
      - ./app:/app
      - firebase-lib:/app/functions/lib
      - firebase-node_modules:/app/functions/node_modules
    environment:
      - FIREBASE_PROJECT
      - FIREBASE_TOKEN
```

node_modulesはホストと共有したくないので、別にvolumesを作ってそこに保持するようにしています。TSのトランスパイル後のコードもホスト側と共有するとホットリロードを妨げるので、同様にしています。
`FIREBASE_TOKEN`はログイン不要でFirebase CLIを叩くためのものです。取得方法は https://firebase.google.com/docs/cli?hl=ja#cli-ci-systems にあります。

```dockerfile:Dockerfile
FROM node:10

RUN apt-get update && \
  npm install -g firebase-tools

WORKDIR /app/functions

COPY package*.json ./

CMD npm install \
  && firebase use ${FIREBASE_PROJECT} --token ${FIREBASE_TOKEN} \
  && npm run emulators

EXPOSE 4000 5000 5001
```
1. `firebase-tools`のインストール
2. npmパッケージのインストール
3. Firebaseプロジェクトの設定
4. エミュレーター（ホットリロード有）の起動

を行っています。ここで`package*.json`をコピーしているのは、`docker-compose.yml`で書いたvolumesのマウントがDockerfileの内容の処理の後になるので、まだDocker内に無いからです。

```:.dockerignore
node_modules
```
ホストのnode_modulesは不要なので、.dockerignoreに書いておきます。

```copy-node_modules.sh
#! /bin/bash

SCRIPT_DIR=$(cd $(dirname $0); pwd)

docker cp -a -L firebase:/app/functions/node_modules $SCRIPT_DIR
```
ホストのVSCodeで開発をするために、Dockerコンテナ内でインストールした`.d.ts`ファイルをホスト側にコピーしてくるスクリプトです。`docker cp`コマンドで拡張子指定とかできると良いんですが、出来ないのでnode_modulesをまるっと持ってきてます。時間かかります。さらにシンボリックリンク関係と思いますが、管理者権限での実行が必要です。

本当はVSCodeの[Remote WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl)プラグインでDockerコンテナ内に接続して開発したいんですが、まだPreview版で動作が安定しないのか、私の環境では頻繁にフリーズしてしまいます…。正式版になったら、またトライしたいと思います。これがうまく使えれば、このスクリプトは不要になりますね。

firebaseコンテナを立ち上げると、OPEN IN BROWSERからエミュレーターのUIへアクセスできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177194/6c490f9d-a30e-f5f1-ff2f-f4aca472e80b.png)

# swaggerコンテナ

```docker-compose.yml
services:
  # 他の設定...
  swagger:
    image: swaggerapi/swagger-ui
    container_name: swagger
    ports:
      - 8000:8080
    volumes:
      - ./swagger:/var/www
    environment:
      SWAGGER_JSON: /var/www/index.yml
```
./swagger/index.ymlファイルを、ホスト側8000ポートで見れるようにします。

swaggerコンテナを立ち上げると、OPEN IN BROWSERからSwagger UIへアクセスできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177194/773e162f-7f4d-d800-f295-dc9b94b14420.png)
このコンテナは単純にindex.ymlを公開しているだけなんで、シンプルですね。

# vuejsコンテナ

```docker-compose.yml
volumes:
  # 他の設定...
  vuejs-node_modules:

services:
  # 他の設定...
  vuejs:
    build: ./app/vuejs
    image: vuejs
    container_name: vuejs
    ports:
      - 3000:3000
      - 3001:3001
    volumes:
      - ./app:/app
      - vuejs-node_modules:/app/vuejs/node_modules
    environment:
      - CHOKIDAR_USEPOLLING=true
```
3000ポートをVue CLI UI用、3001ポートをserve用にします。
こちらでもはやりnode_modulesはホストと共有したくないので別volumesで回避します。
環境変数の`CHOKIDAR_USEPOLLING`はserveでホットリロードするために必要になるもののようです。

```dockerfile:Dofkerfile
FROM node:10

RUN apt-get update && \
    npm install -g @vue/cli

WORKDIR /app/vuejs

COPY package*.json ./

CMD npm install \
    && vue ui --host 0.0.0.0 --port 3000

EXPOSE 3000 3001
```

Vue CLIをインストールし、`vue ui`コマンドのオプションで以下を指定しています

- `--host 0.0.0.0` Dockerコンテナの外からでもWebUIにアクセスできるようにする
- `--port 3000` 3000ポートでWebUIを立ち上げる

```:.dockerignore
node_modules
```
こちらもホストのnode_modulesは不要なので、.dockerignoreに書いておきます。

```vue.config.js
module.exports = {
    devServer: {
        port: 3001,
        host: '0.0.0.0'
    }
}
```
serveで立ち上げるサーバーのオプションを`vue ui`と同様な感じで指定します。

```copy-node_modules.sh
#! /bin/bash

SCRIPT_DIR=$(cd $(dirname $0); pwd)

docker cp -a -L vuejs:/app/vuejs/node_modules $SCRIPT_DIR
```
firebaseコンテナと同じくホストのVSCodeで開発をするために、Dockerコンテナ内でインストールした`.d.ts`ファイルをホスト側にコピーしてくるスクリプトです。時間かかります。管理者権限での実行が必要です。

vuejsコンテナを立ち上げると、`OPEN IN BROWSER`からVue CLI UIへアクセスできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177194/a6f27d4e-0842-03a0-0c65-6b4fe676b532.png)

以上、Dockerを使ったFirebase+Vue.js+TSアプリの開発環境構築でした。今後の各ツールのバージョンアップによってここの内容は変わりそうですが、2020/12時点ではこんな感じになりましたという記事でした。Docker便利ですね。