docker-composeで開発したnginx+node.jsのアプリケーションをDocker Hubで配布する方法

# 要約
#### 配布方法
Dockerfileをdocker-composeでビルドして, nginx由来とnode.js由来の２つのimageを作り, Docker Hubにプッシュする. docker-composeでビルドするときに, buildの中でcontextを使うと便利. 

アプリケーションを作るdocker-compose.yamlの例 
https://github.com/Toyoharu-Nishikawa/jsnote/tree/docker

```
git clone -b docker https://github.com/Toyoharu-Nishikawa/jsnote.git
docker-compose up -d
```


#### 利用方法
利用者がアプリケーションを使うときは, docker-compose.yamlでDocker Hubに登録したimageをpullして使う. 

アプリケーションを使うdocker-compose.yamlの例 
https://github.com/Toyoharu-Nishikawa/jsnote/tree/app

```
git clone -b app https://github.com/Toyoharu-Nishikawa/jsnote.git
docker-compose up -d
```
# 背景
[jsnote](https://toyoharu-nishikawa.github.io/jsnote/)というブラウザで動くJavaScriptのエディタを作っていた. 静的ファイルの配布はnginxで行い, データの保存はnode.jsで行うことにした.


docker-composeでnginxとnode.jsのイメージを立ち上げて, それぞれに必要なソースコードをvolumeでマウントすることでコンテナとファイルを共有し, コーディングをしていた. 

Docker Hubにイメージを登録して, 誰でも簡単に使えるようにしようと思ったけれど, Docker Hubに置くimageをどう作成するべきかわからなかった.

[![Imgur](https://i.imgur.com/HstpPha.png)](https://toyoharu-nishikawa.github.io/jsnote/)

jsnoteについては[こちらの記事](https://qiita.com/Toyoharu-Nishikawa/items/044d53974c42bc635f0f)

# 問題

下記のようなディレクトリ構成の下, public/でフロント側の開発を行い, src/でサーバ側の開発を行っていた場合, gitで管理したまま, public/とsrcを入れたdocker imageを如何に作成するか.
(例: jsnoteの階層の下に全ファイルをがあるとする.)

```
./jsnote
|-- docker-compose.yaml
|-- conf.d
|   |-- server.conf
|
|-- public
|   |-- index.html
|   |-- scripts
|   |-- styles
|
|-- src
|   |-- index.js
|   |-- node_modules
|   |-- package.json
|   |-- package-lock.json

```

開発中のdocker-compose.yamlの内容は下記.

```yaml:docker-compose.yaml
version: '2'
services:
  node:
    image: node 
    restart: always 
    volumes:
      - ./src:/usr/src
      - ./public/sample:/usr/share/nginx/html/sample
      - /var/log/jsnote:/var/log/node
    working_dir: '/usr/src'
    command: npm start
  nginx:
    image: nginx
    restart: always 
    volumes:
      - ./public:/usr/share/nginx/html
      - ./conf.d:/etc/nginx/conf.d
      - /var/log/jsnote:/var/log/nginx
    ports:
      - "2555:80"
```



# 試行
3つの方法を思いついた順に試行した.

## 方法1: nginxとnode.jsをひとつのdockerのimageに入れるDockerfileを作成, build.shでビルドする.  
nginxもしくはnode.jsのイメージを基に, もう一つを入れる. 例えば, node.jsを基に作るとしたら次のようになる.

```dockerfile
FROM node:alpine
以下nginx:alipineのDocker HubのDockerfileの中身をコピペ
.
.
.
RUN rm /etc/nginx/conf.d/*
ADD conf.d /etc/nginx/conf.d
ADD public /usr/share/nginx/html
ADD src/ /usr/src
CMD nginxとnode.js起動するコマンド
```
nginxとnode.jsの２つを起動するコマンドをどう書くべきかで悩んだ. Dockerの公式サイト [Dockerのベストプラクティス](http://docs.docker.jp/engine/articles/dockerfile_best-practice.html)を見て, 方法1を中止した.
> ##コンテナ毎に１つのプロセスだけ実行
ほとんどの場合、１つのコンテナの中で１つのプロセスだけ実行すべきです。アプリケーションを複数のコンテナに分離することは、水平スケールやコンテナの再利用を簡単にします。サービスとサービスに依存関係がある場合は、 コンテナのリンク を使います。

## 方法2: nginxとnode.jsのimageからから別々のimageを作成するようにDockerfileを作成し, 双方をbuild.shでビルドする.
nginxとnode.jsを基にimageを作るDockerfileをそれぞれ用意する.

```dockerfile:web/Dockerfile
FROM nginx:alpine
RUN rm /etc/nginx/conf.d/*
ADD conf.d /etc/nginx/conf.d
ADD public /usr/share/nginx/html
```

```dockerfile:web/build.sh
build -t jsnote_web .
```

```dockerfile:api/Dockerfile
FROM node:alpine
ADD  src/ /usr/src
WORKDIR /usr/src
ENTRYPOINT ["npm","start"]
```

```dockerfile:api/build.sh
build -t jsnote_api .
```

docker-composeでまとめているメリットを活かせないことに気づき, 方法2を中止した.

## 方法3: 方法2をdocker-composeでまとめてビルドする.  
 docker-composeの中でbuildを使うことで, (2)のDockerfileをまとめてbuildできる. しかし, Dockerfileと同じディレクトリにADDするファイルを置かないといけない....  
⇒対処法参照

# 解決法
buildの中で, contextを指定することで, 親ディレクトリのファイルをDockerfileでADDできる. ことを利用すると, 開発中のdirectory階層のままでDockerfileをビルドすることができる.

#### step1: webとapiのディレクトリを追加し, その中にDockerfileを置く. 

```yaml:docker-compose.yaml
./jsnote
|-- docker-compose.yaml
|-- web                   ← 追加
|   |-- Dockerfile        ← 追加
|
|-- api                   ← 追加
|   |-- Dockerfile        ← 追加
|
|-- conf.d
|   |-- server.conf
|
|-- public
|   |-- index.html
|   |-- scripts
|   |-- styles
|
|-- src
|   |-- index.js
|   |-- node_modules
|   |-- package.json
|   |-- package-lock.json
```

#### step2: docker-compose.yamlを下記のようにする.

```yaml:docker-compose.yaml
version: '2'
services:
  api:
    build: 
      context: ./ 
      dockerfile: ./api/Dockerfile
  web:
    build:  
      context: ./ 
      dockerfile: ./web/Dockerfile 
    ports:
      - "2555:80"
```
#### step3: docker-composeでビルドする.
```shell
docker-compose build
```
これでdockerのimageが以下のように2つできる.

- jsnote_web
- jsnote_api


# Docker Hubにpush
docker tagでimage名をDocker Hubに書き換えて, pushする.
#### step1: docker login
予めDocker Hubに作って置いたアカウントにログインする.

```shell
docker login
```
#### step2: docker tag
docker tagでDocker Hubに登録するimageの名前に変更する. この時, ユーザ名/を頭に付ける必要がある.

```shell
docker tag jsnote_web ユーザ名/jsnote_web:latest
docker tag jsnote_api ユーザ名/jsnote_api:latest
```

#### step3: docker push
```shell
docker push ユーザ名/jsnote_web:latest
docker push ユーザ名/jsnote_api:latest
```
以上で, Docker Hubでimageが公開され, 配布できるようになる.

以降は, 誰でもdocker pullでimageをダウンロードできる.

自分で試す場合は, docker rmiでimageを削除してから, pullできるか試す必要がある.

```
docker rmi ユーザ名/jsnote_web:latest
docker rmi ユーザ名/jsnote_api:latest

docker pull ユーザ名/jsnote_web_web:latest
docker pull ユーザ名/jsnote_api:latest
```
# 最後に(アプリケーションの使い方)
nginx+node.jsのアプリケーションとして使うには, docker-composeを使って, 2つをリンクさせる必要がある.

```diretory-tree
./jsnote              
|-- docker-compose.yaml
```

```docker-compose.yaml
version: '2'
services:  
  api:
    image: ユーザ名/jsnote_api
  web:  
    image: ユーザ名/jsnote_web
    ports:
      - "80:80"
```

(注意)
本例のdocker-compose.yamlの場合, nginxのリバースプロキシproxy_pass(ロードバランサ―の設定はupstream)はserverとして, jsnote_api_1もしくは単にapiを参照しなくてはならない. nginxのリバースプロキシの設定はnginxの.confに書かれている. 上記 解決法 で作成したnginxのimageの中に適切なリバースプロキシの設定が書かれていない場合, 作者がconf.d/default.confの中の設定を書き直し, もう一度imageを作り直してDocker Hubにアップロードするか, アプリケーションの利用者がdocker-composeの中でvolumesで設定を上書きする必要がある.