dockerでの環境構築をものすごく詳しく書いてみる in ruby2.6.5 on Rails6

# rails6が出たんですよ
いい加減コード書いてる記事を上げていきたいのですが、相変わらずこんな構築関係だったりミドルウェアの話だったりしか書けていません。良くない。なので、rails6の環境を作ったら、railsの記事とかを上げていきたいと思っています。今回は、その旗揚げのための記事になります。

## Dockerfileとdocker-compose.ymlってどう書かれていくの？
ものすごく詳しく書くとタイトルに書きましたが、端的に言うと、Dockerfileとdocker-composeがどう書かれていって、最終的にrailsが立ちあがるところまでの状態になっていくかってことを書きます。大抵の記事って、完成形がドンとあって、こうしてこうでこう、はい完成！みたいな感じで、一向にプロセスが見えてこないので、？が残りやすい気がするんですよね。え？そんなことない？私はそうなので、書きます。いきなりあんな完璧なDokcerfileとdocker-compose.ymlが読書感想文書くみたいなノリでサクサク書けるわけないんですよ。と言うわけで、今回はどう言う風にファイルが継ぎ足されていって出来上がっていくかを書いていきたいと思います。

## 使用頻度が高いdockerコマンド
とはいえ、docker desktopがあるのでそんなに使わなくなりました。削除とかはdocker desctopでやってしまう方が作業はスムーズです。

```terminal
upしたコンテナの**CONTAINER_ID**を表示する
$ docker ps

buildしたコンテナの**IMAGE_ID**で表示する
$ docker images

プロセスを削除する
$ docker rm CONTAINER_ID

ビルドしたイメージを削除する
$ docker rmi IMAGE_ID
$ docker rmi -f IMAGE_ID
```

## 使用頻度の高いdocker-composeコマンド

```terminal
コンテナをビルドすることでコンテナイメージを作成する
$ docker compose build

ビルドしたコンテナイメージを元にコンテナを作成する
$ docker compose create

# コンテナを起動させる
$ docker compose start

# コンテナを停止させる
$ docker compose stop

# コンテナのログを見る
# オプションfの後にコンテナ名を指定するとそのコンテナ名のログだけをtailする
$ docker compose logs -f (コンテナ名)

# startさせているコンテナ内に入る
$ docker coompose exec [ymlファイルで定義したサービス名] bash
```

`docker compose run` は毎回毎回新規でコンテナを作ってしまうので、非推奨。
`docker compose up` はログを常時出してくれるという点でstartと異なるが、コンテナを立ち上げてlogsを使えばいいことなのでこちらもあまり使わない。

## ステップ1:何も入ってなくていいからbuildできるようにする
ベースイメージはCentOSの8を指定するだけでいきます。何も具体的なものは入っていないコンテナがとりあえず立ちあがるところまで書いていきます。

### Dockerfileを書く

```Dockerfile
FROM centos:centos8
```

### docker-compose.ymlを書く

```docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0
    container_name: rails-sql
    command: mysqld --collation-server=utf8mb4_unicode_ci
    ports:
      - 3307:3307

  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - ./rails-app:/root/rails-app
    ports:
      - 3000:3000
    depends_on:
      - db
```

必要なのは、DBのコンテナと、rails本体が動くwebコンテナなので、それぞれの設定を必要最小限書きます。ここでいう必要最小限は、お互いのコンテナを繋げること、ポートを開けておくこと、upしたらコンテナが立つようになってること、railsアプリの共有化がされるようになっていることです。

## ステップ2:buildしたコンテナ内に入り、環境構築する

```terminal
$ docker run -it image_id /bin/bash
```

ここからはターミナル画面を2画面にスプリットして、片方でコンテナ内で操作、片方でDockerfileに記述する、と言う方法でガンガン作業を進めていきます。

### ruby2.6.5をローカルインストール
rubyのローカルインストールする方法は様々あれど、環境を汚してしまうことを気にしなくていいので、ソースからビルドしてやる方法を取って見たいと思います。

[rubyダウンロード](https://www.ruby-lang.org/ja/downloads/)

上記のサイトのダウンロードリンクをコピーしてきて、それを引数にwgetしてきたら、tarで解凍して、makeでビルドします。
取り敢えず、先ほど作ったDockerfileからイメージを作成し、コンテナ内に入ります。

```terminal
$ docker-compose build
$ docker run -it IMAGE_ID /bin/bash
```

Dockerfileに書いていく内容はこのコンテナ内でrubyをインストールできるまでのプロセス全てになります。

まず、rubyのダウンロードリンクを使ってパッケージを落としてきたいわけですが、wgetコマンドが今のCentOS8には入っていません。試しに打って見てください。command not foundになるはずです。なので、yumを使ってwgetをインストールします。そうして手に入れたtarファイルをtarコマンドで解凍します。makeしてみようとすると、またコマンドが無いのでyumを使ってmakeをインストールします。これでmakeしようとすると、いくつかのパッケージが足らないと言われ、エラーが出ます。その足りていないパッケージはエラーを読むとgcc zlib-devel openssl-devel readline-devel libffi-develですので、これもyumで全部インストールします。これでようやくrubyをインストールすることができます。

これをプロセスとして正しい処理順に並び替えて、Dockerfileに記述します。必要なパッケージが全てインストールされた状態から、wget、tar、makeの順で実行されるようにします。

```Dockerfile
FROM centos:centos7

RUN echo 'export LC_ALL=C' >> /root/.bash_profile && \
    yum clean all && \
    yum update -y && \
    yum install wget -y && \
    yum install make -y && \
    yum install gcc zlib-devel openssl-devel readline-devel libffi-devel -y && \
    cd usr/local/src/ && \
    wget https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.5.tar.gz && \
    tar -zxvf ruby-2.6.5.tar.gz && \
    cd ruby-2.6.5 && \
    ./configure && \
    make && \
    make install
```

すると、ここまででruby2.6.5が入った状態のdockerイメージがbuildされるようになることが保証されます。

### 完成したGemfileとGemfile.lockを共有化
完成したGemfileというのは、rails newをして作られた時にできるGemfileということです。
では、早速コンテナの中に入って、bundle initします。Gemfileをviで開き、railsのコメントアウトを外し、bundle updateをかけたら、足りないパッケージ、sqlite-develがエラーで表示されたので、これをインストールします。そして
bundle installをします。すると、nokogiriで落ちます。エラーを見ると足りてないパッケージ、libxml2-devel libxslt-develがあったので、これをyumでインストールします。かつ、nokogiriはオプション付きで単体インストールしないと入らなかったので、こいつはdockerfileでもbundle install前に個別に書きます。

以上の作業を行うことで、綺麗にbundle installできるようになったGemfileをコンテナ内で作ることができました。このGemfileを永続化しますので、docker cpを使用します。

```terminal
$ docker cp CONTAINER_ID:/root/rails-app-name/Gemfile ./
$ docker cp CONTAINER_ID:/root/rails-app-name/Gemfile.lock ./
```

これでローカルにGemfileとGemfile.lockが出来上がり、永続化ができました。ここからrailsアプリ本体を扱えるようにDockerfileを記述していきます。

bundle initするために作ったディレクトリを作らせ、WORKDIRをrailsのアプリを置いたところへ設定します。
ここにGemfileとGemfile.lockをaddされるように指定し、nokogiriをオプション付きでインストールしたコマンドを実行し、最後にbundle installをするように設定します。そして最後にrails本体がaddされるようにします。すると下記のようになります。

```Dockerfile
FROM centos:centos8

RUN echo 'export LC_ALL=C' >> /root/.bash_profile && \
    yum clean all && \
    yum update -y && \
    yum install wget -y && \
    yum install make -y && \
    yum install gcc zlib-devel openssl-devel readline-devel libffi-devel -y && \
    yum install libxml2-devel libxslt-devel -y && \
    yum install sqlite-devel -y && \
    cd usr/local/src/ && \
    wget https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.5.tar.gz && \
    tar -zxvf ruby-2.6.5.tar.gz && \
    cd ruby-2.6.5 && \
    ./configure && \
    make && \
    make install

RUN  mkdir /root/scheran-app
WORKDIR /root/scheran-app
ADD Gemfile /root/scheran-app/Gemfile
ADD Gemfile.lock /root/scheran-app/Gemfile.lock
RUN gem install nokogiri -- --use-system-libraries=true --with-xml2-include=/usr/include/libxml2/ && \
    bundle update && bundle install

ADD . /
```

### railsアプリ本体を共有化してdocker-compose upの準備
docker-compos.ymlにvolumesを加えてこの後に行うwebpackerをインストールできるようにしてからコンテナ内でrails newをしでできたrails本体をdockr cpして、rails本体の共有化をできるようにします。

```docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0
    container_name: rails-sql
    command: mysqld --collation-server=utf8mb4_unicode_ci
    ports:
      - 3307:3307

  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/root/scheran-app/
    ports:
      - 3000:3000
    depends_on:
      - db
```

### webpackerをinstallできるようにする
bundle installすると、このような下記の表示が出ているので、これを修正します。rails6からはwebpackerが備え付けで入っているので、これを入れられるようにしていきます。

```
Use `bundle info [gemname]` to see where a bundled gem is installed.
         run  bundle binstubs bundler
         run  bundle exec spring binstub --all
* bin/rake: Spring inserted
* bin/rails: Spring inserted
       rails  webpacker:install
sh: node: command not found
sh: nodejs: command not found
Node.js not installed. Please download and install Node.js https://nodejs.org/en/download/
```

まずは、nodejsをソースからビルドしていき、そこで一緒に入ってくるnpmを使ってyarnをインストールすることができれば、webpackerはインストールすることができますので、nodejsをビルドする際に必要なパッケージを先にyumでインストールします。必要なパッケージは./configureを実行してcommand not foundって言われたやつをパカパカ入れて行くのと、プ公式のインストール方法を読めば絶対に入ります。公式だけ読めばいいじゃんと思うかもしれませんが、意外と環境によっちゃ入ってないものがあってかつ公式では入ってること前提で端折られていることもあるので、双方向からやる必要があります。そして、今回必要だったパッケージは、gcc-c++、which、python2でした。これをインストールして、wgetでソースコードを引っ張ってきて、tarで解凍、然るのちにmakeでビルドしてやればnodejsのインストールは完了するので、npmが使えるようになります。これでyarnを入れて、最後にrails webpacker:installを実行してwebpackerをインストールしてあげれば、rails6の環境構築が完了したコンテナイメージが作れるDockerfileが出来上がります。

以上の記述を書き起こしたDockerfileが下記になります。これで完成です。

```Dockerfile
FROM centos:centos8

RUN echo 'export LC_ALL=C' >> /root/.bash_profile && \
    yum clean all && \
    yum update -y && \
    yum install wget -y && \
    yum install make -y && \
    yum install gcc zlib-devel openssl-devel readline-devel libffi-devel -y && \
    yum install libxml2-devel libxslt-devel -y && \
    yum install sqlite-devel -y && \
    yum install gcc-c++ which python2 -y && \
    yum clean all && \
    cd usr/local/src/ && \
    wget https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.5.tar.gz && \
    tar -zxvf ruby-2.6.5.tar.gz && \
    wget https://nodejs.org/dist/v12.13.0/node-v12.13.0.tar.gz && \
    tar -xvf node-v12.13.0.tar.gz && \
    rm -rf ruby-2.6.5.tar.gz && \
    rm -rf node-v12.13.0.tar.gz && \
    cd ruby-2.6.5 && \
    ./configure && \
    make && \
    make install && \
    cd .. && \
    cd node-v12.13.0 && \
    ./configure && \
    make -j4 && \
    make install && \
    npm install --global yarn -y

RUN  mkdir /root/scheran-app
WORKDIR /root/scheran-app
ADD Gemfile /root/scheran-app/Gemfile
ADD Gemfile.lock /root/scheran-app/Gemfile.lock
RUN gem install nokogiri -- --use-system-libraries=true --with-xml2-include=/usr/include/libxml2/

ADD ./scheran-app /root/scheran-app
RUN  bundle update && bundle install && rails webpacker:install
```

### mysqlの設定をする
database.ymlにデータベースの情報を記述します。情報を隠蔽する為に.envで読ませる形にしたいと思います。

```docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0
    container_name: rails-sql
    command: mysqld --collation-server=utf8mb4_unicode_ci
    ports:
      - 3307:3307
    env_file: .env
```

```.env
SQL_USER=user_name
MYSQL_ROOT_PASSWORD=password
```

以上のようにします。これによってmysqlの初期化が行われます。

### コンテナ内に入ってrails new .
ここでコンテナに入って、rails new .をしてあげます。webpackerもこの時に一緒に入ってくれます。できたrailsのプロジェクトをdocker cpしてローカルに置いときます。前の段階で共有化する準備はできているので、これで殆ど準備完了です。

### Dockerコンテナのメンテナンス
最後に、初期でcpしたADDしたGemfileとGemfile.lockの部分を、rails newで作成してできたプロジェクトの中にあるGemfileへと変更します。

そうしないと、docker-compose.ymlで書いたvolumeしている部分が常にコンテナを立ち上げた時の状態になってしまいます。

> volumeのdirectoryはいい感じにファイルをマージしてくれているわけではなく、ディレクトリごと上書きしてしまいます

なので、アップデートがgemやyarnなどにあった場合、buildしたコンテナ内ではDockerfileによって、bundle installやyarn upgradeされていたとしても、**addされた内容が古い状態になってしまっている**ので、そこでバージョンに差異ができてしまい、upできなくなる現象が発生します。

ここがこの先、開発をしていく時のコンテナのメンテナンス部分になってくると思います。新しくgemを追加した時はもちろん、gemの仕様変更などでbundle installしてgemが更新された時などでコンテナが立ち上がらなくなる可能性があります。

```Dockerfile
ADD Gemfile /root/scheran-app/Gemfile
ADD Gemfile.lock /root/scheran-app/Gemfile.lock
```

これを・・・

```Dockerfile
ADD  ./scheran-app/Gemfile /root/scheran-app/Gemfile
ADD  ./scheran-app/Gemfile.lock /root/scheran-app/Gemfile.lock
```

にします。そうすることで、今後、gemやyarnにアップデートがあった場合、コンテナをrunで立ち上げて、手動でbundle installしたり、yarn upgradeをかけた時でも、自動的にvolume先に更新部分が反映されます。

## 作ったコンテナでupしてみる
作られたコンテナをupしてrailsサーバーにアクセス、loclahost:3000でrailsの画面が出たら、完成です。

## 感想
細かくやってみたものの、結構詰まってしまった。いつになったらサクサク環境構築できるようになるのやら・・・
docker-compose upの時のエラー(exit code ~)はdocker-compose.ymlの記述ミスを疑わないとね・・・当たり前だけど・・・

## docker用途別記事

コンテナを作成中に使うコマンドや運用中に使うコマンド、エラーが起きたら使うコマンドの記事は下記にまとめています。

[増えすぎたnoneのdockerイメージや、バックで動くコンテナプロセスを全て削除する](https://qiita.com/Nedward/items/6e9d0e862bfa47379ab5)

[Dockerのボリューム先を削除してコンテナが立ち上がるようにする](https://qiita.com/Nedward/items/b81fbb8dbe71a4d85082)

[デーモンコンテナを完全に削除する方法](https://qiita.com/Nedward/items/1f3416ea644a487e6d5f)

[docker cp使って簡単永続化](https://qiita.com/Nedward/items/d64749f447c9e11f411c)