Vue.jsのMarkdownアプリをコンテナで作った話

# 概要

Vue.jsでサンプルアプリを作成しました。

そのサンプルアプリを作成した時に、
コンテナで環境構築した際の手順をメモとして残しておきます。
Vue.jsでのアプリ構築手順とDocker/Docker Composeのインストール手順については触れません。

<img width="628" alt="mymarkdown.png" src="https://qiita-image-store.s3.amazonaws.com/0/314463/f42c5e5c-fe4e-e17a-b141-bd6d51f9579f.png">

mymarkdownのアプリケーションスタックは下記になります。
・[Vue.js](https://jp.vuejs.org/index.html)
・[Firebase authentication](https://firebase.google.com/docs/auth/?hl=ja)
・[Firestore](https://firebase.google.com/docs/firestore/?hl=ja)


> サンプルアプリ構築にあたっては、初モダンJavaScriptなのもあって下記の本を参考に進めました。
> 私のようにモダンJavaScriptを初めて触る方にオススメです。
>
> [Vue.jsとFirebaseで作るミニWebサービス (技術書典シリーズ（NextPublishing](https://www.amazon.co.jp/gp/product/B07HXMHQQK)

# 環境

開発環境はコンテナ、実行環境はFirebase、エディターはVisual Studio Codeを利用しました。

・Mac Pro(2018) OS Mojave 10.14.1
・Docker for Mac(Docker Engine 18.09)

> コンテナなので、Dockerさえ動けばどの環境でも再現できます

# コンテナで作った理由

コンテナで作成した理由として、ざっくりと以下の理由があります。

1. ミドルウェア(Node)とフレームワーク(Vue)の変遷が早すぎて、
　　　　　　メジャーバージョンが出版時とかけ離れている
2. 1に関連して、別のフレームワークやNodeのバージョンが違うアプリを構築していると端末のバージョン管理がカオスになる
3. **コンテナが大好き**

という理由で環境構築をDockerコンテナで作成しました。

# vue-cliコンテナの作成

今回作成したマークダウンアプリは、Node.js + Vue.jsで動くため、
そのコンテナを作成します。
ソースは[GitHub](https://github.com/govargo/vue-cli/tree/2.5)にも上げています。

コンテナは下記のバージョンで構築しました。

|            | バージョン     |
|:-----------|:------------|
| Node.js    | 8.11.2      |
| vue-cli    | 2.5         |

下記がvue-cliコンテナのDockerfileになります。

```dockerfile:Dockerfile
FROM node:8.11.2

WORKDIR /usr/src/app

# Install Vue-cli
RUN npm i -g vue-cli@2.5

# Show vue-cli command
CMD ["vue","-h"]
```

Dockerイメージは[DockerHub](https://cloud.docker.com/u/govargo/repository/docker/govargo/vue-cli)にプッシュしてあります。

コンテナをDockerコマンドで実行します。
-vオプションを指定して、作成したmymarkdownプロジェクトを永続化しておきます。
※これをやらないとコンテナ破棄時にプロジェクトも消えます

```
$ docker run -it --rm -v $(pwd):/usr/src/app govargo/vue-cli:v2.5 bash  
root@e82fba0d3103:/usr/src/app# vue init webpack-simple mymarkdown
 ? Project name mymarkdown
 ? Project description A Vue.js project
 ? Author username <name@example.com>
 ? License MIT
 ? Use sass? Yes

root@e82fba0d3103:/usr/src/app# cd mymarkdown
root@e82fba0d3103:/usr/src/app# npm install
```

「Ctrl + P」「Ctrl + Q」でコンテナからホストに抜けて、コンテナを停止します。

```
$ docker stop <コンテナID>
```

# Docker Composeでホットリロード開発

続いて、先ほどのイメージ(govargo/vue-cli:v2.5)とは別のコンテナを作成します。

用途としては
・必要なnodeモジュールをインストールしておく(毎回コンテナ起動後にインストールすると手間)
・Firebaseへのデプロイ用にfirebase-toolsを入れておく
ためです。

```dockerfile:Dockerfile
FROM govargo/vue-cli:v2.5

COPY mymarkdown/package*.json ./
RUN npm install
RUN npm install -g firebase-tools

EXPOSE 8080

CMD ["npm", "run", "dev"]
```

こちらはDockerHubに入れていないので、コンテナイメージのビルドが必要になります。
-t以下でコンテナイメージのタグ名を記載します。
今回は「govargo/mymarkdown:v1.0」にしました。

```
$ docker build -t govargo/mymarkdown:v1.0 .
```

ビルドが終わった後に起動しますが、
毎回Dockerの実行コマンドを打つのも手間なので、
Docker Composeで起動します。

使用するdocker-compose.ymlは下記です。
実行するとDockerfileの末尾にあるコマンドの「npm run dev」が起動します。

```yml:docker-compose.yml
version: '3'
services:
  mymarkdown:
    image: govargo/mymarkdown:v1.0
    ports:
      - 8080:8080
      - 9005:9005
    volumes:
      - "./mymarkdown:/usr/src/app"
```

Docker Composeで起動します。

```
$ docker-compose up
Creating network "mymarkdown_default" with the default driver
Creating mymarkdown_mymarkdown_1 ... done
Attaching to mymarkdown_mymarkdown_1
mymarkdown_1  |
mymarkdown_1  | > mymarkdown@1.0.0 dev /usr/src/app
mymarkdown_1  | > cross-env NODE_ENV=development webpack-dev-server --open --hot
mymarkdown_1  |
mymarkdown_1  | Project is running at http://localhost:8080/
mymarkdown_1  | webpack output is served from /
mymarkdown_1  | Content not from webpack is served from dist
mymarkdown_1  | 404s will fallback to /index.html
mymarkdown_1  | Unable to open browser. If you are running in a headless environment, please do not use the open flag.
```

[http://localhost:8080/]()で起動したとメッセージが出ますが、
この状態では端末のブラウザからはアクセスできません。
Ctrl + CでDocker Composeを終了しておきます。

これはコンテナの仕様で、コンテナ内のlocalhostと端末のlocalhostが、
同一のネットワークではなく、外部ネットワークと見做されるからです。
また、開発モードのWebpackはlocalhostのみからの通信しか許可しません。
(このlocalhostとはコンテナ内のlocalhstであって、ホストのlocalhostではありません)

> vue-cliの新しいバージョン(3)で利用する「vue create <app-name>」では
> この問題は発生しないそうです

コンテナ内からはlocalhost宛に通信するとHTMLが返却されますが、
コンテナ内にはブラウザがないので画面が見れません。困ります。

そこで外部ネットワークからでもアクセスできるよう、Webpack側の設定を変更します。
「npm run dev」コマンド実行時に、
「cross-env NODE_ENV=development webpack-dev-server --open --hot」
というコマンドが実行されていますが、このコマンドに下記オプションを追加します。
「--host 0.0.0.0」

コマンドに上記オプションを追加するのには、package.jsonを更新します。

```diff:package.json
{
  "name": "mymarkdown",
  "description": "A Vue.js project",
  "version": "1.0.0",
  "author": "govargo",
  "license": "MIT",
  "private": true,
  "scripts": {
-    "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot,
+    "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot --host 0.0.0.0,
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
  },
  "dependencies": {
    "shitajicss": "^5.0.0",
    "vue": "^2.5.11"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ],
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.0",
    "babel-preset-stage-3": "^6.24.1",
    "cross-env": "^5.0.5",
    "css-loader": "^0.28.7",
    (省略)
```

package.jsonを更新をしたところで、再びDocker Composeで起動します。

```
$ docker-compose up
Creating network "mymarkdown_default" with the default driver
Creating mymarkdown_mymarkdown_1 ... done
Attaching to mymarkdown_mymarkdown_1
mymarkdown_1  |
mymarkdown_1  | > mymarkdown@1.0.0 dev /usr/src/app
mymarkdown_1  | > cross-env NODE_ENV=development webpack-dev-server --open --hot --host 0.0.0.0
mymarkdown_1  |
mymarkdown_1  | Project is running at http://0.0.0.0:8080/
mymarkdown_1  | webpack output is served from /
mymarkdown_1  | Content not from webpack is served from dist
mymarkdown_1  | 404s will fallback to /index.html
mymarkdown_1  | Unable to open browser. If you are running in a headless environment, please do not use the open flag.
```

[http://localhost:8080/]()をブラウザで開くと、下記の画面が表示されます。

<img width="569" alt="スクリーンショット 2019-01-20 0.40.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/314463/2886318a-64f6-5bf9-5b7f-6a694371a9cb.png">

開けない場合は一度ブラウザを終了するかPCを再起動してください。

> 個人的に躓いた箇所ですが、8080ポートにTCPソケットのゴミが大量に残っており、
> 画面に繋がりませんでした。
>
> 3日ほど原因が分からずハマっていましたが、
> 再起動して(プロセスが初期化されたことで)無事に画面が開きました。

Dockerでコンテナとホストのファイルを共有した状態で、
Webpackのホットリロード機能が動いているので、
コンテナ上で動いているのに関係なくHTMLやVueファイルを更新するとホットリロードが実施されます。

# Firebaseでのデプロイ

Firebaseへのデプロイもコンテナ内から行います。
※Firebaseでの登録やタグ情報の埋め込みは終わっている前提で話を進めます。

### Firebaseへのログイン

firebaseにログインするため、コンテナにログインします。

```
# コンテナIDの特定
$ docker ps

# コンテナへのログイン
$ docker exec -it <コンテナID> bash
```

コンテナ内でfirebase loginコマンドを実行します。

```
root@9f92530aee77:/usr/src/app# firebase login
? Allow Firebase to collect anonymous CLI usage and error reporting information? Yes

Visit this URL on any device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&scope=email%20openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloudplatformprojects.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Ffirebase%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&response_type=code&state=861427277&redirect_uri=http%3A%2F%2Flocalhost%3A9005

Waiting for authentication...
```

URLが標準出力に出るので、URLをコピーしてブラウザに貼り付けてアクセスします。
Googleのアカウント認証画面に遷移するので、認証をします。
認証が成功すると、Firebaseにログインできます。

アカウント認証が終わると[http://localhost:9005]()にリダイレクトするため、
docker-compose.ymlにて事前に9005ポートを開けています。

認証成功後に、Firebaseにログインしたという通知が標準出力に吐かれます。

```
Waiting for authentication...

✔  Success! Logged in as <ユーザー名>
```

> CI用にログインする「login:ci」というコマンドもありますが、
> CIを回すほどの長期開発ではなかったため、今回は記載していません。

この後はfirebase initで初期化、firebase deployでデプロイを行なっていきます。
ここから先はコンテナ、VM、物理関係なく同一の手順が続くので今回の記事では省略します。

# 終わりに

コンテナを利用すると、商業本と実際のバージョンが乖離していて、謎のエラーに苦しむ...
この本の環境を作って、あの本の環境を作っていたらミドルウェアがバッティングして動かなくなった...
などの悩みは減ると思います。

充実したコンテナライフを送りましょう！

以上