はじめてのDocker In Windows

## 概要
この記事は、初めてDockerを触って複数のコンテナを連携させたAPIサーバー環境を
作成するところまでの手順をまとめたメモです。
これから初めてWindowsでDockerを触ろうという方の取っ掛かりになれば幸いです。

## 環境
- Windows10
- git bash
- Windows Terminal
- Rancher Desktop

## 準備
Dockerを使うために [Rancher Desktop](https://rancherdesktop.io/) をインストールします。

インストーラをダウンロードして起動し、最後までインストールが完了したらターミナルで`docker`コマンドが使えるかの確認を行います。

```bash
$ docker --version
Docker version 20.10.17-rd, build c2e4e01
```
無事にインストールできたようです。

## 普通にコンテナを作成してみる。
Apacheがインストールされたコンテナを作成し、ブラウザでアクセスできるところまでやってみます。

### イメージファイルをdocker hubから取得
Dockerで使用するイメージファイルは、[Docker Hub](https://hub.docker.com/)からdockerコマンドを使用して取得することができます。

```bash
# docker pull {イメージ名}:{タグ} タグを省略した場合はデフォルトのタグになります。大抵はlatestです。
# 最新版を指定した場合は更新に伴い動作しなくなる可能性があるので、ちゃんとした環境を整える場合はバージョンを指定してあげてください。
$ docker pull httpd:latest
```
### 取得できているか確認
```bash
$ docker images
REPOSITORY                         TAG       IMAGE ID       CREATED         SIZE
httpd                              latest    8653efc8c72d   6 days ago      145MB
rancher/mirrored-library-traefik   2.9.1     e6de8578b238   6 weeks ago     107MB
rancher/mirrored-coredns-coredns   1.9.1     99376d8f35e0   8 months ago    49.5MB
rancher/mirrored-metrics-server    v0.6.1    e57a417f15d3   9 months ago    68.8MB
rancher/klipper-lb                 v0.3.5    dbd43b6716a0   10 months ago   8.09MB
rancher/local-path-provisioner     v0.0.21   fb9b574e03c3   11 months ago   35MB
k8s.gcr.io/pause                   3.6       6270bb605e12   15 months ago   683kB
```
さっき取得した`httpd`が表示されているので、問題なく取得できています。

他にも公式のコンテナイメージがありますので、[Docker Hub](https://hub.docker.com/)か、`docker search {検索ワード}`でイメージの検索をすることができます。

### イメージからコンテナを作成
次に取得したイメージを元にコンテナを作成します。
```bash
# docker create {イメージ名}
$ docker create httpd:latest
849ae7bc74da850bf6305e9679a32912f6746f37169b37419b3d327d7ab274f3
```
生成されているか確認します。
```bash
$ docker ps -a
CONTAINER ID   IMAGE                              COMMAND                  CREATED          STATUS                       PORTS     NAMES
849ae7bc74da   httpd:latest                       "httpd-foreground"       30 seconds ago   Created                                frosty_visvesvaraya
```
Rancher Desktopを使用している場合は、いくつかのコンテナが常駐しているので、余計なものは省いています。
今回作成したコンテナ名は`frosty_visvesvaraya `となっており、ランダムで命名されてしまっているので、扱いやすくするために明示的に名前を付けてみます。

先程作成したコンテナは削除しておきます。
```bash
# docker rm {コンテナ名}
$ docker rm frosty_visvesvaraya
frosty_visvesvaraya
```
削除ができたら、改めて--nameオプションを付けて再度生成してみます。
```bash
# docker create --name {コンテナ名} {イメージ名}
$ docker create --name my-apache-container httpd:latest
c71a552e998966a5a0b9e97259293c90b842b28ae9d6218645328783e12845d9

$ docker ps -a
CONTAINER ID   IMAGE                              COMMAND                  CREATED          STATUS                       PORTS     NAMES
c71a552e9989   httpd:latest                       "httpd-foreground"       11 seconds ago   Created                                my-apache-container
```
今度は自分でつけた名前である、`my-apache-container`となっていますね。

### コンテナを起動する
先程の存在確認のコマンドでは、STATUSがCreatedになっていると思います。
この状態はコンテナの作成だけされていて、起動されていない状態を指します。
次にこのコマンドでコンテナを起動します。
```bash
$ docker start my-apache-container
my-apache-container

$ docker ps -a
CONTAINER ID   IMAGE                              COMMAND                  CREATED              STATUS                       PORTS     NAMES
c71a552e9989   httpd:latest                       "httpd-foreground"       About a minute ago   Up 24 seconds                80/tcp    my-apache-containe
```
今度はStatusが起動中を示すUpになっています。

それでは実際にアクセスしてみます。
http://localhost をブラウザから開いてみますが、ページが開けずエラーになってしまいました。

これはコンテナのポートフォワーディングの設定ができていないことが要因なので、今度はポートの設定を追加して再生成します。

### コンテナのポートを開放する。
まずは先程作成したコンテナを停止・削除します。
```bash
# docker stop {コンテナ名}
$ docker stop my-apache-container
my-apache-container

$ docker rm my-apache-container
my-apache-container
```
今度はさらに-pオプションを付けて再度コンテナを作成します。
```bash
$ docker create --name my-apache-container -p 8080:80 httpd:latest
$ docker start my-apache-container
my-apache-container
```
`-p 8080:80`は、ホスト側のポートを8080番に設定し、そのポートでアクセスした際にコンテナ側の80番のポートに接続する設定です。

この状態で再度アクセスをしますが、今度はポート番号を指定したURLでアクセスします。
http://localhost:8080 にアクセスして、 `It works!` と表示されれば成功です！

もし、以下のようなエラーが出る場合は、既に他のアプリやサービスがホスト側に指定したポート番号を使用しているので、設定を変えてみましょう。
```bash
Error response from daemon: driver failed programming external connectivity on endpoint my-apache-container (c3ebb54a1c5030c38f7a6ef74d0732c326328ee7003eddbfc011c50e82ae8575): Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use
```

## コンテナに入って作業してみる
次は作成したコンテナに入って作業してみます。
```bash
# docker exec -it {コンテナ名} {実行コマンド}
$ docker exec -it my-apache-container bash
```
このコマンドは、指定したコンテナで指定したコマンドを実行するというものになるので、
bashではなくて他のコマンドを指定した際は、コンテナの外からコンテナ内でコマンドを実行させることもできます。
正常にコンテナに入ることができていれば以下のような表示なると思います。
```bash
root@9fcef18256cc:/usr/local/apache2#
```
コンテナに入ることができたので、Apacheのスタートページを編集してみます。
しかし、コンテナにはvimがインストールされていないので、vimをインストールします。
```bash
root@9fcef18256cc:/usr/local/apache2# apt-get update
Get:1 http://deb.debian.org/debian bullseye InRelease [116 kB]
Get:2 http://deb.debian.org/debian-security bullseye-security InRelease [48.4 kB]
Get:3 http://deb.debian.org/debian bullseye-updates InRelease [44.1 kB]
Get:4 http://deb.debian.org/debian bullseye/main amd64 Packages [8184 kB]
Get:5 http://deb.debian.org/debian-security bullseye-security/main amd64 Packages [200 kB]
Get:6 http://deb.debian.org/debian bullseye-updates/main amd64 Packages [14.6 kB]
Fetched 8607 kB in 1s (7605 kB/s)
Reading package lists... Done

root@9fcef18256cc:/usr/local/apache2# apt-get install -y vim
```
これでvimがインストールできているはずなので、index.htmlを編集して、再度 http://localhost:8080 にアクセスします。
編集した内容が反映されていることが確認できれば大丈夫です。
index.htmlは`/usr/local/apache2/htdocs/`に置いてあります。

次の作業の前に環境を掃除します。
```bash
#コンテナ停止
$ docker stop my-apache-container

#コンテナ削除
$ docker rm my-apache-container

#イメージ削除
$ docker rmi httpd:latest
```

## runで起動する
ここまでの内容は、`docker run`コマンドで`pull → create → start` まで自動で行ってくれます。
```bash
$ docker run --name my-apache-container -p 8080:80 httpd:latest
```

しかし、使用するイメージによってはこのようにフォアグラウンドで実行されている扱いになり、操作を受け付けてくれなくなってしまうため、気を付けましょう。
```bash
$ docker run --name my-apache-container -p 8080:80 httpd:latest
Unable to find image 'httpd:latest' locally
latest: Pulling from library/httpd
a603fa5e3b41: Pull complete
4691bd33efec: Pull complete
ff7b0b8c417a: Pull complete
9df1012343c7: Pull complete
b1c114085b25: Pull complete
Digest: sha256:f2e89def4c032b02c83e162c1819ccfcbd4ea6bdbc5ff784bbc68cba940a9046
Status: Downloaded newer image for httpd:latest
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
[Wed Nov 16 06:47:15.435670 2022] [mpm_event:notice] [pid 1:tid 139859256008000] AH00489: Apache/2.4.54 (Unix) configured -- resuming normal operations
[Wed Nov 16 06:47:15.435757 2022] [core:notice] [pid 1:tid 139859256008000] AH00094: Command line: 'httpd -D FOREGROUND'
```
こうなってしまった場合はCtrl+cなどの操作も受け付けてくれない為、別ウィンドウから該当のコンテナを停止してください。

次の作業の為にコンテナを掃除します。

## Dockerfileを使った構築
先程作成したイメージにはvimがインストールされていない為、あとから自分でインストールする必要がありました。
今度は、vimをインストールしてあるオリジナルのイメージを作成してみます。

PCの好きな場所に任意の名前のフォルダを作成し、その中に`Dockerfile`という名前のファイルを作成します。
今回は`apache-vim`というディレクトリ名で作成しています。
ファイルの内容は以下になります。
```dockerfile
FROM httpd:latest

RUN apt-get update # リポジトリなどの更新
RUN apt-get install -y vim # vimをインストール
```
こちらは先程使用したhttpd(Apache)のイメージを元に、コンテナにvimをインストールするコマンドを追加したものです。

FROMは元となるイメージを指定する記述で、pullやcreateと同じようにイメージ名とタグを指定します。
1から全部指定したい倍は、scratchを指定することで空の状態から始めることができます。

RUNはそのコンテナ内で実行するコマンドの記述です。

#を付けることでコメントにできます。
他にもいろいろできることはありますが、今は使わないので割愛します。

ここで作成したDockerfileからイメージを作成します。
Dockerfileが置いてあるディレクトリに移動し、以下のコマンドを実行します。
```bash
# docker build -t {イメージ名} {Dockerfileのパス}
$ docker build -t my-apache-vim-image .

$ docker images
REPOSITORY                         TAG       IMAGE ID       CREATED          SIZE
my-apache-vim-image                latest    a547adb04b35   29 seconds ago   200MB
```
イメージ一覧に自作のイメージが追加されていれば大丈夫です。
そして、このイメージからコンテナを作成・起動してアクセスして動作確認をします。

```bash
$ docker create --name my-apache-container -p 8080:80 my-apache-vim-image
8bdcb0c7b5d056a8c6bbe479b360962d20fbaf09fb8464c5c01090ab4cd908be

$ docker start my-apache-container
my-apache-container

#コンテナにログイン
$ docker exec -it my-apache-container bash
# エラーが出なければOK
root@8bdcb0c7b5d0:/usr/local/apache2# vim
```

## ホストのファイルやディレクトリをコンテナにマウントする
実際に運用する場合、今のままではいちいちコンテナに入ってファイルの修正をする必要があり、またコンテナを作り直すたびにファイルの修正も戻ってしまうので、ホストPCのファイルやディレクトリをコンテナと共有して永続化します。

マウントは、コンテナ作成時に`-v`オプションを付けることでできます。

まずは共有するファイルをホスト側に作成します。
今回は、Apacheのウェルカムページを作成します。
ディレクトリ構成は以下のようにしておきます。
- apache-vime/
    - Dockerfile
    - source/
        - index.html
このファイルを、コンテナ側のドキュメントルートにある、`/usr/local/apache2/htdocs/index.html`にマウントします。

```bash
$ docker create --mount type=bind,source={マウント対象のフルパス},destination=/usr/local/apache2/htdocs --name my-apache-container -p 8080:80 my-apache-vim-image
$ docker start my-apache-container
```
一度この状態で http://localhost:8080 アクセスして動作確認をしてみます。

表示はされるものの、自分の環境では文字化けを起こしていました。
折角なので、このままindex.htmlをホスト側で編集して文字化けを直してみましょう。
```html
<html>
    <header>
        <title>DockerTest</title>
    </header>
    <body>
        <h1>ファイルマウントテスト</h1>
        <p>このファイルはホストPCにあるindex.htmlです</p>
    </body>
</html>
```
↓
```html
<html lang="ja">
    <meta charset="utf-8">
    <header>
        <title>DockerTest</title>
    </header>
    <body>
        <h1>ファイルマウントテスト</h1>
        <p>このファイルはホストPCにあるindex.htmlです</p>
    </body>
</html>
```
これで再度 http://localhost:8080 にアクセスすると文字化けが解消していました。
このまま一度コンテナを再作成してみて、この変更が維持されていることを確認できれば、マウントの動作確認はOKです。

ここまでの無いようで、単一のコンテナでできることはおおよそできていると思います。

#### コンテナが多くなりすぎて困ったときに役立つコマンド
```bash
# イメージをすべて削除
docker image prune
# コンテナをすべて削除
docker volume prune
# 全削除してリセット
docker system prune -a
# 複数のコンテナをまとめて管理しているときはこちら
docker compose down系 

# 全てのコンテナを停止する
docker stop $(docker ps -q)
# 全てのコンテナを削除する
docker rm $(docker ps -q -a)
# 全てのイメージを削除する
docker rmi $(docker images -q) -f
```
いくつかまとめて一つのコマンドにしておくと便利です。
```bash
alias docker-purge='docker stop $(docker ps -q) && docker rm $(docker ps -q -a) && docker rmi $(docker images -q) -f'
```

## 複数のコンテナを利用する
実際には複数のコンテナを組み合わせて動かすことが多いので、複数のコンテナを管理する方法を試してみます。

今度は`docker compose`コマンドを使用して、複数のコンテナ同士の依存関係や設定をymlファイルに記述して、一括で制御できるようにします。

PHP+Apacheのコンテナと、MySQLのコンテナの二つを作成して連携させてみます。

新しく作業用のディレクトリを作成して、そこに以下のファイルを作成します。
今回は`multi-container-test`というディレクトリ名で作業します。
- multi-container-test
    - docker-compose.yml
    - .env // 環境変数を定義しておくファイル

docker-compose.ymlは以下の内容で記述します。
```yaml
version: '3'

services:
  php:
    image: php:5.5-apache
    depends_on:
      - mysql
    ports:
      - "8080:80"
    env_file: .env

  mysql:
    image: mysql:5.7
    volumes:
      - "./.data/db:/var/lib/mysql"
    env_file: .env
```
ここで、このファイルについての簡単な解説をしておきます。
- version: 定義ファイルの書式バージョン(現在の最新は3.4だそうです)
- services: アプリケーションの要素名
    - image: ベースとするコンテナイメージの指定
    - depends_on: コンテナの依存関係の指定(今回の例ではapacheのコンテナがmysqlサービスに依存しているので、mysql → apache という順番で処理されます)
    - ports: 起動時のポート設定(-pオプションと同じです)
    - volumes: マウントの設定(-vオプションと同じです)
    - env_file: 設定される環境変数を定義したファイルの指定

環境変数定義ファイル(.env)は以下の内容になっています。
```
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=test
MYSQL_USER=test
MYSQL_PASSWORD=password
```

ここまで出来たら、docker-compose.ymlのあるディレクトリで以下のコマンドを実行します。
```bash
$ docker compose up -d

# 起動しているコンテナを確認する
$ docker compose ps -a
NAME                            COMMAND                  SERVICE             STATUS              PORTS
multi-container-test-apache-1   "docker-php-entrypoi..."   apache              running             0.0.0.0:8080->80/tcp, :::8080->80/tcp
multi-container-test-mysql-1    "docker-entrypoint.s..."   mysql               running             3306/tcp, 33060/tcp
```
`docker compose up` の `-d`オプションを付けないと、フォアグラウンドで実行されてしまうので、注意してください。
こちらの場合はCtrl+cで停止することができますので、そこまで面倒なことにはならないかと思います。

次にコンテナ間の疎通確認を行うので、phpコンテナに入ります。
```bash
$ docker compose exec -it php bash
```
そのまま接続確認を行うためにpingをインストールして実行します。
宛先はymlファイルで指定したservice名です。
```bash
root@37d91bfa187e:/var/www/html# apt update
root@37d91bfa187e:/var/www/html# apt -y install iputils-ping

root@37d91bfa187e:/var/www/html# ping mysql
PING mysql (172.28.0.2) 56(84) bytes of data.
64 bytes from multi-container-test-mysql-1.multi-container-test_default (172.28.0.2): icmp_seq=1 ttl=64 time=0.036 ms
64 bytes from multi-container-test-mysql-1.multi-container-test_default (172.28.0.2): icmp_seq=2 ttl=64 time=0.043 ms
64 bytes from multi-container-test-mysql-1.multi-container-test_default (172.28.0.2): icmp_seq=3 ttl=64 time=0.043 ms
```
このように表示されれば接続ができているので成功です。
また、ホスト側に起動したディレクトリに、`.data/db`が存在していればデータベースの内容の永続化もできています。

今度は `mysql` コンテナに入って、mysqlの動作確認を行います。

mysqlへrootでログインするためのパスワードは、`.env`ファイルに記載した`MYSQL_ROOT_PASSWORD=root`です。
また、`.env`に記載した`MYSQL_DATABASE=test`のtestDBも作成されているはずです。

```bash
$ docker compose exec -it mysql bash
bash-4.2# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.40 MySQL Community Server (GPL)

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| test               |
+--------------------+
5 rows in set (0.02 sec)
```
これでmysqlコンテナの確認もできました。

次はapacheコンテナからmysqlコンテナに接続して、PHPからデータを取得できるようにしてみます。

### docker compose から Dockerfileで作成した自作イメージを起動する
apacheコンテナからmysqlに接続するためには、今のままではいくつか拡張機能が足りていないので、それらをインストールしたイメージを自作して、`docker compose` に組み込みます。

ディレクトリ構成を以下のように変更します。
- multi-container-test
    - my-apache
        - Dockerfile
        - src
            - index.php
    - .env
    - docker-compose.yml

Dockerfileを作成します。
ちなみにテストで使用しているPHPイメージがPHP5.6なのは作ろうとしていた環境が古いからです。
特に気にせず新しいもので試してみてください。
```dockerfile
FROM php:5.6-apache

RUN apt update
RUN apt install -y vim
RUN docker-php-ext-install pdo pdo_mysql
```
phpの拡張機能のインストールは、通常の方法ではできないので、`docker-php-ext-install` を使用します。
今回はvimと、PHP拡張機能のpdo, pdo_mysqlをインストールします。

ちなみに、Docker上で使用できる拡張機能には制限があり、使用できるものは以下のものになります。
```
Possible values for ext-name:
bcmath bz2 calendar ctype curl dba dom enchant exif fileinfo filter ftp gd gettext gmp hash iconv imap interbase intl json ldap mbstring mcrypt mysqli oci8 odbc opcache pcntl pdo pdo_dblib pdo_firebird pdo_mysql pdo_oci pdo_odbc pdo_pgsql pdo_sqlite pgsql phar posix pspell readline recode reflection session shmop simplexml snmp soap sockets spl standard sysvmsg sysvsem sysvshm tidy tokenizer wddx xml xmlreader xmlrpc xmlwriter xsl zip
```

次に`docker-composer.yml`を修正します。
```yaml
version: '3'

services:
  apache:
    build: "./my-apache"
    depends_on:
      - mysql
    ports:
      - "8080:80"
    volumes:
      - "./my-apache/src:/var/www/html/"
    env_file: .env

  mysql:
    image: mysql:5.7
    volumes:
      - "./.data/db:/var/lib/mysql"
    env_file: .env
```
変更箇所は、apacheのimageを削除し、buildで先程作成したDockerfileを配置したディレクトリを指定して、Dockerfileをビルドさせるようにします。
また、動作確認の為にvolumesでindex.phpをApacheのウェルカムページに配置しています。
httpdとは違うイメージを使用しているので、配置するディレクトリも異なっています。

index.phpは現在のPHPの設定を確認することが目的なので、以下のようにPHPの情報だけ表示するようにしておきます。
```php
<?php
phpinfo();
```

この状態で一度、すべてのコンテナとイメージを削除してから、再度`docker compose up -d` を実行します。
起動が出来たら、 http://localhost:8080 にアクセスしてみて、PHP情報の中にpdo_mysqlの項目が表示されていれば準備完了です。

次に、mysqlコンテナに入ってテスト用のテーブルを作成してデータを入れます。
今回は以下のようなテーブルを作成して確認します。
```sql
CREATE TABLE test (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `title` varchar(255) NOT NULL,
    `description` text NOT NULL,
    `limit_date` datetime NOT NULL,
    `is_completed` tinyint(1) unsigned NOT NULL DEFAULT '0',
    `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB CHARSET=utf8;

INSERT INTO test (title, description, limit_date) VALUES
 ("Task1", "description a", "2022-12-31 16:00:00"),
 ("Task2", "description b", "2022-12-14 13:00:00"),
 ("Task3", "description c", "2022-11-30 12:00:00");
```
そしてindex.phpを書き換えて、mysqlからデータを取得して表示するように修正します。
```php
<?php
$dns = "mysql:dbname=test;host=mysql;charset=utf8;port=3306";
$user = "root";
$password = "root";
$pdo = new PDO($dns, $user, $password);
$stmt = $pdo->prepare("SELECT * FROM test");
if (!$stmt->execute()) {
    throw new Exception("falied execute query");
}
$data = $stmt->fetchAll();
var_dump($data);
```
これで http://localhost:8080 にアクセスしてみます。
正常にデータが取得できているようで、先程用意したデータがJSON形式で表示されました。

最後に、コンテナを再生成してデータの永続化ができていることを確認します。
```bash
$ docker compose down
$ docker compose up -d
```

これで基本的なDockerの使い方は試せていると思います。

## まとめ
Windowsだけ特殊文字(改行など)が異なるのでそこだけ注意が必要ですが、レンタルサーバーなどを借りずにサーバーサイドの開発ができる良い仕組みだと思います。

より詳しい内容や、他の機能などもあるので是非公式のリファレンスなどを見ながらいろいろと試してみてください。
最近C#のサーバーサイド開発フレームワークのASP.netと.Net Core が正式にUbuntuに対応し、さらにDockerもサポートしてくれるとのことなので、絶賛サーバーサイドC#も実験中です。