今更聞けないDockerのしくみ(「Dockerとは？」から「docker-composeファイルを1人で作れるようになる」まで)

# はじめに
なんとな〜くdockerを使い始めてはや４年ほど。
既存のプロジェクトにアサインされた場合はdockerファイルに何が記載されているかなんて意識せずコマンドを実行するだけで、何か自分で一から作る時は、誰かが作ったものをどこからか持ってきて済ませていた。

こんな感じなのでdockerをなんとなく扱えてはいるが細かいところを全く理解できてない。
今回は人に説明できるくらい理解できるようになろうとした男の記事です。

**ハンズオン形式でやっていきますので一緒に手を動かしながらやってみていただけると嬉しいです。**



## 対象とする読者
- これからdockerをは0から理解したい人
- なんとなくdocker触っちゃってて理解していない俺みたいな人
(でもLinux多少知っていないと少し大変かもです)
- Docker, docker image, docker container, docker-composeといわれてもパッとイメージがつかない俺みたいなレベル感の人
※rubyとrailsをベースに解説を行いますがエッセンスとなる部分はrubyユーザーでなくても参考になる部分は多いと思います。

### Dockerとは?
※もっと説明上手い人たくさんいるのでさらっといきます

:::note info
**Dockerとは?**

Dockerはコンテナ仮想化プラットフォームのこと。
開発環境をコンテナにパッケージ化することで、開発者間で一貫性のある環境を確保でき、コンテナはどのプラットフォームでも動作するため、異なる環境での問題を最小限に抑えることができる。

![Docker勉強.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/ab16684b-2258-cabd-0a5d-23f1b10aee83.jpeg)
:::

### 実際にdocker imageをbuild→docker containerを起動させてみよう
docker containerを起動するには`docker image`が必要です。
`docker image`とは`docker container`の雛形です。
`docker image`を作成するためには`Dockerfile`を用意します。
`Dockerfile`はいわば`docker image`の「設計図」のようなものです。

#### ①docker imageのビルド



では任意のディレクトリに作業ディレクトリを作りましょう。僕は`docker_sample`というディレクトリで作業しようと思います。
```
mkdir docker_sample

cd docker_sample
```

続いて必要なファイルを用意します。

```
$ touch Dockerfile

$ mkdir app

$ touch app/main.rb
```

ディレクトリが以下のようになっていればOKです。

```
$ find .                                                                     
.
./app
./app/main.rb
./Dockerfile
```

実際のファイルには以下のように記述します。

```Dockerfile:Dockerfile
FROM ruby:2.7

WORKDIR /var/www
# /var/wwwの部分は/myappなどなんでも良い
COPY ./app /var/www
```

以下のコマンドを実行します。
`$ docker image build -t sample_app:latest .`

すると以下のように表示されます。
```
[+] Building 3.3s (8/8) FINISHED                                      docker:desktop-linux
 => [internal] load build definition from Dockerfile                                  0.0s
 => => transferring dockerfile: 143B                                                  0.0s
 => [internal] load .dockerignore                                                     0.0s
 => => transferring context: 2B                                                       0.0s
 => [internal] load metadata for docker.io/library/ruby:2.7                           3.1s
 => [internal] load build context                                                     0.0s
 => => transferring context: 57B                                                      0.0s
 => [1/3] FROM docker.io/library/ruby:2.7@sha256:2347de892e419c7160fc21dec721d595273  0.0s
 => CACHED [2/3] WORKDIR /var/www                                                     0.0s
 => [3/3] COPY ./app /var/www                                                        0.0s
 => exporting to image                                                                0.0s
 => => exporting layers                                                               0.0s
 => => writing image sha256:4aff41a0a11e60c78693c2d4bb5b0e00cfdbe98747c725a03666e4df  0.0s
 => => naming to docker.io/library/sample_app:latest   
```

#### 解説
`Dockerfile`の解説
![スクリーンショット 2023-10-15 14.27.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/36246427-0006-839c-dca9-54ce07f5c32e.png)

コマンドの解説
![スクリーンショット 2023-10-17 21.23.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/503c285e-f1e4-2f45-2f03-cf13e3e2b6e7.png)

ビルドしたdocker imageを確認するにはこのコマンドを実行します
`$ docker image ls`
実行すると以下のようなイメージがビルドされていることがわかります。
先ほど設定した`sample_app`、`latest`という値が設定されていることがわかります。
![スクリーンショット 2023-10-17 21.36.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/eb4cb11f-4676-0534-3206-d108a395b072.png)

#### ②docker containerの起動

Dockerにはライフサイクルがあります

:::note info
docker containerの作成
↓
docker containerの起動
↓
docker container停止
↓
docker container削除

![Docker勉強 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/26ed3169-6074-c39d-f37f-1e21b9c96237.jpeg)
:::

docker imageを作成してからdocker containerを作成、起動する必要があるということです。

今回はdocker containerが起動した際に`app/main.rb`の`"Hello World!"`が実行されるようにしてみます。


```ruby:main.rb
p "Hello World!"
```

```Dockerfile:Dockerfile
FROM ruby:2.7

WORKDIR /var/www
# /var/wwwの部分は/myappなどなんでも良い
COPY ./app /var/www

CMD ["ruby", "/var/www/main.rb"]
```
以下の一行を追加しdocker containerが起動した時に実行したいコマンドを記述しておきます。
`CMD ["ruby", "/var/www/main.rb"]`

あくまでdocker container上でコマンドを実行するためディレクトリはDockerfileで記述した`/var/www`内の`main.rb`になっているということですね。

`Dockerfile`を編集したのでdocker imageを再度ビルドします。

`$ docker image build -t sample_app:latest .`

ビルドが完了したらいよいよdocker containerを起動します。

`$ docker container run --name sample_app sample_app:latest `
![スクリーンショット 2023-10-17 22.29.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/da8e8cd8-85fc-d9cb-ee56-7fb71b7ca427.png)

`run`というコマンドはコンテナの**作成＋起動**を意味するコマンドです。(のちのdocker-composeの話にも共通します）
```
$ docker container run --name sample_app_container sample_app:latest    

"Hello World!"
```

`main.rb`に記述してある`"Hello World!"`がコールされます。

docker containerの起動を確認するには以下のコマンドを実行します。
```
$ docker container ls

CONTAINER ID　　IMAGE COMMAND　　CREATED  STATUS PORTS NAMES
```
あれ、起動したはずなのに何も表示されていませんね。

これは`p "Hello World!"`という処理が完了したためdocker containerが自動的にストップしている状態です。

停止したコンテナも確認するには`-a`オプションをつける必要があります。
```
$ docker container ls　-a

CONTAINER ID   IMAGE                           COMMAND                   CREATED         STATUS                     PORTS                               NAMES
084abfd63748   sample_app:latest               "ruby /var/www/main.…"   5 minutes ago   Exited (0) 4 minutes ago                                       sample_app_
```
`Exited`となっているので停止していることがわかりますね。

docker containerの説明をしたいのに我ながら例が悪かったと思いますのでファイルを修正して再度試してみましょう。


```ruby:main.rb
p "Hello World!"

sleep(100)
```

このようにすれば100秒間はコンテナが起動し続けますのでこれで確認してみましょう。

一度docker containerを削除します。
```
$ docker container rm sample_app_container
sample_app_container
```

docker imageも再度ビルド
```
$ docker image build -t sample_app:latest .
```

再度コンテナを起動します。
```
$ docker container run --name sample_app_container sample_app:latest    
```

ここで`docker container ls`を実行しようとすると...
あ、ターミナルが入力を受け付けてくれませんね。
docker containerのログが出力されるようになっているためです。

別タブを開いてもいいですが、バックグラウンドでコンテナを起動するモードがあるのでそれを試してみましょう。

docker containerコマンドを実行する際に`-d`オプションをつける必要があります。

```
$ docker container run -d --name sample_app_container sample_app:latest
0cc709282c933d71b424b0856eef0eb12d974ca4f3fb31604aeb7eec874a28d8
```

バックグラウンドでコンテナが起動しているか確認してみます。

```
$ docker container ls
CONTAINER ID   IMAGE                           COMMAND                   CREATED         STATUS                 PORTS                               NAMES
0cc709282c93   sample_app:latest               "ruby /var/www/main.…"   3 minutes ago   Up 8 seconds                                               sample_app_cont
```

起動が確認できました。

また起動中のコンテナに対してコマンドを実行するには以下のようにします

```
$ docker container exec sample_app_container ruby -v
ruby 2.7.8p225 (2023-03-30 revision 1f4d455848) [aarch64-linux]
```

`docker container exec` + `コンテナ名` + `〇〇`
とすることでコンテナの中のファイルにアクセスできます。

#### コンテナを停止→削除

コンテナを停止→削除するには以下の手順です。

停止
```
$ docker container stop sample_app_container
sample_app_container
```
削除
```
$ docker container rm sample_app_container
sample_app_container
```

#### gemを使ってみよう

もう少し実践的にしてみます。docker環境上でgemを使うとしましょう。

もうお気づきの方もいるかもしれませんが先ほどの挙げた`docker container exec` + `コンテナ名` + `〇〇`というコマンドを応用して
`docker container exec bundle install`などとすればgemが利用できそうですよね。
ただ、それだとdocker containerを起動するたびにそのコマンドを実行しなければならず、そんなことやってられないですよね。

そこでDockerfileに追記を行います。

```Dockerfile:Dockerfile
FROM ruby:2.7

WORKDIR /var/www
# /var/wwwの部分は/myappなどなんでも良い
COPY ./app /var/www
#gemをインストールする先のパスを指定する(あまり深く考えなくていいです)
RUN bundle config --local set path 'vendor/bundle'

RUN bundle install

CMD ["ruby", "/var/www/main.rb"]
```

`RUN`というコマンドを利用することでDockerコンテナイメージをビルドする際に実行するコマンドを指定できます。

`Gemfile`も作り忘れちゃいけませんね。今回はかの有名な`nokogiri`を例にとってgemが利用できることを確認していきます。

```Gemfile:app/Gemfile
source "https://rubygems.org"

gem 'nokogiri'
```
`main.rb`を以下のように書き換えます。

```ruby:app/main.rb
require 'nokogiri'
require 'open-uri'

# ウェブページを取得(今回はwikipediaを例に取ります)
doc = Nokogiri::HTML(open('https://ja.wikipedia.org/wiki/'))

# タイトルを表示
puts doc.title
```

:::note alert
ウェブスクレイピングは、サイトの利用規約に適合し、倫理的に行わなければならないことに注意して行う必要があります。違法行為や悪意のある活動は絶対に行わないようにしましょう。
:::

再度イメージをビルドし、コンテナを起動してみましょう。

`docker image build -t sample_app:latest .`を実行してみるとさっきDockerfileに追記したコマンドが実行されていることがわかります。
![スクリーンショット 2023-10-18 21.54.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/d1eb6582-1d6c-1ab2-a9c7-fe550fae1b2a.png)

コンテナを起動します
```
$ docker container run --name sample_app_container sample_app:latest
/var/www/main.rb:5: warning: calling URI.open via Kernel#open is deprecated, call URI.open directly or use URI#open
Wikipedia
```

gemを利用して`Wikipedia`というtitleが取得できていることがわかります。
`/var/www/main.rb:5: warning: calling URI.open via Kernel#open is deprecated, call URI.open directly or use URI#open`
という警告が出ていますがあくまでサンプルアプリなのでそこはご愛嬌でお願いします。(gemが使えることをおみせしたかっただけなのです:bow_tone1:)

### docker composeとは?
ここまで長々と書いてきて「実務環境で使う`docker-compose`いつ出てくるんだ？？？？」と思われていると思います。。。

# いよいよ本題のdocker-composeです

:::note warn
現在ではdocker-composeコマンドよりdocker composeコマンドが推奨されています。

従来の docker-compose コマンドと新しい docker compose コマンドのどちらを使用するかは、状況に依存します。ただし、Dockerは将来的には新しい docker compose コマンドを推奨する方向に進んでおり、新しいプロジェクトでは docker compose コマンドの使用を検討することが一般的です。
:::


実務で使うアプリケーションだと
- webサーバー
- データベース

など複数のアプリケーションやミドルウェアが一つになって一つのシステムが出来上がっていると思います。
(ほかにも例を挙げるならバッチ処理やフロントエンドのアプリケーションなど...)
それらのアプリケーションごとにDockerfile用意してコンテナ起動して...なんてやっていられないですよね。相互の連携まで考えたら頭が痛くなります。

**ここで`docker-compose`が必要になってくるわけです。**

:::note info
docker composeはdockerコンテナの集合体
:::

### 実際にdocker-composeを利用してみよう
ここからは`rails`を題材にwebアプリケーションをdocker環境で構築できるようになってみたいと思います。

別のディレクトリを用意していただいた方がわかりやすいと思うので僕は今回は`docker_rails_sample`というディレクトリを用意しました。以下のファイルを用意してください。

```dockerfile:Dockerfile
FROM ruby:2.7

WORKDIR /var/www

COPY ./app /var/www

RUN bundle config --local set path 'vendor/bundle'

RUN bundle install
```
Dockerfileはもう解説不要ですよね。

```Gemfile:app/Gemfile
source "https://rubygems.org"

gem 'rails'
```

```yml:docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./app/db/mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - ./app:/var/www
    ports:
      - "3000:3000"
    environment:
      RAILS_ENV: development
    depends_on:
      - db
```
#### 解説
![スクリーンショット 2023-10-19 1.55.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/8bab714c-4e64-cada-dbb3-fa728872eb12.png)

#### rails newを実行
まだgemfileしかないので`rails new`をdocker環境に対して行います
`$ docker-compose run web rails new . --force --database=mysql`
![スクリーンショット 2023-10-19 8.29.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/fa649967-0d05-09b0-310d-f200293c3ad8.png)

ずらっとファイル群が生成されていますね。
(ここまでやってきて今更ですが自作したappディレクトリにrailsによって生成されたappディレクトリがあり、名前がややこしいですね...反省🙇)
![スクリーンショット 2023-10-19 8.33.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/cde7322d-ea0e-23db-08bd-7450c795ecf6.png)

これによりDockerfileやGemfileの内容が更新されているので例によって再ビルドを行います。

`$ docker-compose build`

```yml:app/config/database.yml
# (中略)

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
# ↓ここに先ほどdocker-compose.ymlファイルで指定したMYSQL_ROOT_PASSWORDの値を入
  password: password
# ↓ここに先ほどdocker-compose.ymlファイルで指定したdbというコンテナ名を指定
  host: db

# (中略)
```

railsのdbを作成するコマンドを実行します
`$ docker-compose run web rails db:create`
![スクリーンショット 2023-10-19 8.52.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/ec43732f-7f82-7f9c-546f-c6298fe04e53.png)

dbの作成ができました。

いよいよです。

`$ docker-compose up`
アクセスしてみると
http://localhost:3000/

![スクリーンショット 2023-10-19 8.55.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/36d1c217-7067-b012-cccd-bb1338370238.png)

できました〜〜:clap:

# 終わりに
どうでしょう。ここまでくれば実務で見かける`Dokcerfile`や`docker-compose.yml`ファイルをみて「あ！これってこういうことしてるんだ！」と今までわからなかったことがわかるようになっているのではないでしょうか。
僕自身前までは「なんだかいろいろやってんだな...」くらいの理解で自分の中で完全にブラックボックス化していましたが、今回腰据えて一から勉強してみて非常に有意義だったのでぜひ駆け出し・新卒エンジニアの皆さんは僕のように4年も放置せずキャッチアップしてもらいたいです。

最後まで読んでいただきありがとうございました。

#### 参考文献
https://www.youtube.com/watch?v=lZD1MIHwMBY&t=6938s

https://www.konosumi.net/entry/2023/02/26/142508