VirtualBoxとDockerでお勉強用の環境を作ってみた

# VirtualBoxとDockerでお勉強用の環境を作ってみた

- 気になるソフトウェアを動かして、挙動を見てみたい。
- ソフトウェアの設定を変えていろいろ実験したい
- 複数のサーバーをつかうつもりでやってみたい
- 失敗時に自力で環境を戻す自信がない（戻すのがめんどくさい）ので、環境の再作成を頻繁にしたい

など思ったときに使っている、お試し環境の構築手順です。主に自宅などでのお勉強での使用を想定しています。

仮想マシンをVirtualBoxで作成し、そこにDockerをインストールします。

Dockerを使うと、プロセスを隔離環境（コンテナ）で実行できます。仮想マシンと違い、コンテナは気軽に作成/削除できるため、失敗を気にせずに気軽にいろいろ実験するにはもってこいです。

## 環境

| 種別 | OS | 備考 |
| --- | --- | --- |
| ホスト（物理マシン） | Windows 10 Home 1909 | インターネットへ接続可能 |
| ゲスト（仮想マシン） | CentOS Linux release 7.8.2003 | - |

## 目次

- 1. 仮想マシンの構築
  - 1.1 VirtualBoxのインストール
  - 1.2 CentOS7イメージのダウンロード
  - 1.3 仮想マシンの作成
  - 1.4 ネットワーク接続設定
  - 1.5 CentOS7インストール
  - 1.6 インターネット接続の確認
- 2. CentOS 7初期設定
  - 2.1 カーネル最新化
  - 2.2 パッケージの更新など
  - 2.3 ホスト-ゲスト共有フォルダの有効化
  - 2.4 一般ユーザーの作成
  - 2.5 SSH設定
  - 2.6 mDNS設定
- 3. Visual Studio Codeの導入
- 4. 開発環境の構築
  - 4.1 Gitの最新化
  - 4.2 Dockerのインストール
  - 4.3 Python3のインストール
  - 4.4 docker-composeのインストール
  - 4.5 Visual Studio Code拡張機能インストール
- 5. デモ
  - 5.1 最低限のWebサイトを構築してみる
  - 5.2 Apache Web Server + Tomcatで最小Webアプリを作ってみる
- 6. まとめ

## 1. 仮想マシンの構築

### 1.1 VirtualBoxのインストール

[VirtualBox](https://www.virtualbox.org/)は仮想化ソフトウェアです。Windows用のインストーラーをダウンロードしてインストールします。インストール先はどこでもよいですが、ここでは`C:\App\VirtualBox`にインストールします。

### 1.2 CentOS7イメージのダウンロード

CentOS 7のDVDイメージを以下のURLからダウンロードします。時間がかかるので、気長に待ちます。

https://www.centos.org/download/

※ CentOS 8が既にリリースされていますが、まだDockerが公式にサポートしていません。

### 1.3 仮想マシンの作成

VirtualBoxマネージャーを開きます。`新規`をクリックします。

![001_vm001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/218f7751-4294-d100-a2ec-21aec65b4e1f.png)

仮想マシンの名前と保存先を指定します。今回は以下のように設定します。

![001_vm002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/916c4c99-d763-821e-eff1-4263c788cc14.png)

メモリは2.0GBとします。必要に応じて変えてください。

![001_vm003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/8bfa9af5-a52b-cd4f-21f8-54b106db1d7d.png)

仮想ハードディスクを固定サイズ32GBで作成します。Dockerイメージ分の容量を確保するため、このくらいにしておきます。

![001_vm004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/3cdfdc7d-fce5-ac68-9533-50455ac71007.png)
![001_vm005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/d7ac748c-6afe-f21c-ec7c-a05c1e46a2b2.png)
![001_vm006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/2546a039-bea0-594a-5d3c-24013fd73896.png)
![001_vm007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/c40bc4f7-6554-cc3b-e0b3-092a4c91c483.png)

以上で仮想マシンの作成が終わりました。

### 1.4 ネットワーク接続設定

仮想マシンがホストのネットワークにブリッジ接続するようにします。ホストからインターネット接続できるのであれば、仮想マシンからもインターネット接続できるようになると思います。

![002_setting001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/e4628fa8-c89c-b300-765e-ad54d2eb478e.png)

### 1.5 CentOS 7インストール

先ほどダウンロードしたCentOS 7のイメージを仮想マシンの光学ドライブにセットします。

![002_setting002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/db872320-0863-655c-eb0a-854fe0e80252.png)

仮想マシンを起動してCentOS 7をインストールします。ネットワークの接続はインストール時にオンにしておきます。

![003_install001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/92ff5830-0618-4c4e-6100-979c3142c1fa.png)

### 1.6 インターネット接続の確認

rootユーザーで仮想マシンにログイン後、適当なインターネット上のWebサイトにアクセスできることを確認します。

```shell-session
# curl --head https://www.google.com/
HTTP/1.1 200 OK
Date: Sat, 14 Mar 2020 16:32:34 GMT
...
```

## 2. CentOS 7初期設定

不安であれば、作業前に仮想マシンのスナップショットを取っておくとよいです。

### 2.1 カーネル最新化

なんとなくカーネルを最新化してみます。以下を参考に実施していきます。

- [ELRepo : HomePage](https://elrepo.org/tiki/tiki-index.php)
- [ELRepo : kernel-ml](https://elrepo.org/tiki/kernel-ml)

今回のカーネルのバージョンは以下です。

- デフォルト：3.10.0
- 最新：5.7.6

まずはELRepoを有効化します。

```shell-session
# yum install https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
```

kernel-mlをインストールします。

```shell-session
# yum --enablerepo=elrepo-kernel install kernel-ml
```

```shell-session
# yum list installed kernel*
読み込んだプラグイン:fastestmirror
Loading mirror speeds from cached hostfile
 * base: ftp.riken.jp
 * elrepo: ftp.yz.yamagata-u.ac.jp
 * extras: ftp.riken.jp
 * updates: ftp.riken.jp
インストール済みパッケージ
kernel.x86_64                              3.10.0-1127.el7                       @anaconda
kernel-ml.x86_64                           5.7.6-1.el7.elrepo                    @elrepo-kernel
kernel-tools.x86_64                        3.10.0-1127.el7                       @anaconda
kernel-tools-libs.x86_64                   3.10.0-1127.el7                       @anaconda
```

再起動後、新しいカーネル(5.7.6)で起動します。再起動後に以下のコマンドでカーネルのバージョンが最新版になっていることを確認します。

```shell-session
# uname --kernel-release
5.7.6-1.el7.elrepo.x86_64
```

残りの最新版カーネルパッケージをインストールします。`yum swap`を使って、古いカーネルパッケージの削除と新しいカーネルパッケージのインストールを同時に行います。

```shell-session
# yum --enablerepo=elrepo-kernel \
  swap \
  -- install kernel-ml-* \
  -- remove kernel kernel-tools kernel-tools-libs
```

※ `yum swap`を使わずにそのままインストールしようとすると、古いものと依存関係が競合して失敗します。

```txt
エラー: kernel-ml-tools conflicts with kernel-tools-3.10.0-1127.13.1.el7.x86_64
エラー: kernel-ml-tools-libs conflicts with kernel-tools-libs-3.10.0-1127.13.1.el7.x86_64
```

これで新しいカーネルのインストールが完了しました。以下で確認します。

```shell-session
# yum list installed kernel*
読み込んだプラグイン:fastestmirror
Loading mirror speeds from cached hostfile
 * base: ftp.riken.jp
 * elrepo: ftp.yz.yamagata-u.ac.jp
 * extras: ftp.riken.jp
 * updates: ftp.riken.jp
インストール済みパッケージ
kernel-ml.x86_64                       5.7.6-1.el7.elrepo      @elrepo-kernel
kernel-ml-devel.x86_64                 5.7.6-1.el7.elrepo      @elrepo-kernel
kernel-ml-doc.noarch                   5.7.6-1.el7.elrepo      @elrepo-kernel
kernel-ml-headers.x86_64               5.7.6-1.el7.elrepo      @elrepo-kernel
kernel-ml-tools.x86_64                 5.7.6-1.el7.elrepo      @elrepo-kernel
kernel-ml-tools-libs.x86_64            5.7.6-1.el7.elrepo      @elrepo-kernel
kernel-ml-tools-libs-devel.x86_64      5.7.6-1.el7.elrepo      @elrepo-kernel
```

### 2.2 パッケージの更新など

以下を実行して、CentOS 7のパッケージを最新化します。

```shell-session
# yum update
```

後で使うのでnet-toolsをインストールします。netstatコマンドが使えるようになります。

```shell-session
# yum install net-tools
```

終わったら、一度ゲストを再起動しておきます。

### 2.3 ホスト-ゲスト共有フォルダの有効化

ホスト-ゲストの間でフォルダの共有を行うことで、ファイルのやり取りを簡単にします。以下の情報を参考に実施します。

- [Chapter 4. Guest Additions](https://www.virtualbox.org/manual/ch04.html)

ゲストにVirtualBox - Guest Additionsのインストールが必要です。

まずは必要なパッケージをインストールします。

```shell-session
# yum groupinstall "development tools"
```

次に、Guest Additionsのディスクイメージを挿入します。ディスクイメージはWindowsホストであれば、VirtualBoxのインストールディレクトリにあります。

![004_addition001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/c9e8ea61-4787-717b-85c5-8b16b6b9ab3e.png)

以下のようにマウントします。

```shell-session
# mkdir /media/cdrom
# mount /dev/cdrom /media/cdrom
mount: /dev/sr0 is write-protected, mounting read-only
```

```shell-session
# ls -l /media/cdrom
合計 45705
-r--r--r--. 1 root root      763  2月 20 22:27 AUTORUN.INF
dr-xr-xr-x. 2 root root     1824  5月 15 03:49 NT3x
dr-xr-xr-x. 2 root root     2652  5月 15 03:49 OS2
-r--r--r--. 1 root root      547  5月 15 03:49 TRANS.TBL
-r--r--r--. 1 root root  3755310  5月 15 03:41 VBoxDarwinAdditions.pkg
-r-xr-xr-x. 1 root root     3949  5月 15 03:41 VBoxDarwinAdditionsUninstall.tool
-r-xr-xr-x. 1 root root  6737330  5月 15 03:42 VBoxLinuxAdditions.run
-r--r--r--. 1 root root  9317888  5月 15 04:43 VBoxSolarisAdditions.pkg
-r-xr-xr-x. 1 root root 16832808  5月 15 03:46 VBoxWindowsAdditions-amd64.exe-r-xr-x
r-x. 1 root root  9863040  5月 15 03:45 VBoxWindowsAdditions-x86.exe
-r-xr-xr-x. 1 root root   270616  5月 15 03:43 VBoxWindowsAdditions.exe
-r-xr-xr-x. 1 root root     6384  5月 15 03:42 autorun.sh
dr-xr-xr-x. 2 root root      792  5月 15 03:49 cert
-r-xr-xr-x. 1 root root     4821  5月 15 03:42 runasroot.sh
```

Guest Additionsをインストールします。以下のコマンドを実行します。

```shell-session
# cd /media/cdrom
# sh ./VBoxLinuxAdditions.run
```

共有フォルダを設定するために、一度仮想マシンをシャットダウンします。終わったら仮想マシンの設定画面から共有フォルダの設定を開き、以下のように設定します。パスはお好みで設定してください。

![005_shared001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/83d0e30e-5e3f-054a-6ca5-e43f17adf917.png)

- ホスト（Windows）のフォルダ：`C:\shared\centos7_sandbox`
- ゲスト（CentOS 7）のマウント先ディレクトリ：`/mnt/host`

これで指定したフォルダをホスト-ゲスト間で共有することができるようになりました。

### 2.4 一般ユーザーの設定

常にrootユーザーで作業するのは好ましくないです。root権限が不要の場面では一般ユーザーを使います。

ユーザーを作成します。名前はすきにつけましょう。ここではrolengraysとします。パスワードも設定しましょう。

```shell-session
# useradd --user-group rolengrays
# passwd rolengrays
```

2.3節で設定した共有フォルダにアクセスするためには、`vboxsf`グループに入る必要があります。

```shell-session
# usermod -aG vboxsf rolengrays
```

rolengraysユーザーでsudoできるようにします。sudoersファイルの文法エラーを防ぐため、visudoコマンドから編集します。

```shell-session
# visudo -f /etc/sudoers.d/rolengrays
```

ファイルの内容は以下の一行のみとします。

```sudoers:/etc/sudoers.d/rolengrays
rolengrays ALL = (ALL) ALL
```

### 2.5 SSH接続設定

2.4節で作成したユーザーでホストからSSH接続できるようにします。3節で使います。

**ホスト**で、SSH用鍵を作成します。最近のWindows10にはデフォルトでSSHクライアント(OpenSSH)がインストールされています。

```cmd
（ホストで実行）
C:\> ssh-keygen -t rsa -b 2048 -N "" -f %USERPROFILE%\.ssh\rolengrays.key
```

**ホスト**で、公開鍵のほうを共有フォルダにコピーします。

```cmd
（ホストで実行）
c:\> copy %USERPROFILE%\.ssh\rolengrays.key.pub C:\shared\centos7_sandbox
        1 個のファイルをコピーしました。
```

**ゲスト**で、公開鍵認証の有効化するため、`/etc/ssh/sshd_config`を編集して`PubkeyAuthentication yes`のコメントアウトを外します。

```diff
（ゲストで実行）
...

- # PubkeyAuthentication yes
+ PubkeyAuthentication yes

...
```

終わったらsshdを再起動して設定を反映します。

```shell-session
（ゲストで実行）
# systemctl restart sshd
```

以下のようにrolengraysユーザーの公開鍵を登録します。パーミッションは正しく設定しないとSSH接続が失敗します。

```shell-session
（ゲストで実行）
# su - rolengrays
$ mkdir --mode 700 ${HOME}/.ssh
$ cat /mnt/host/rolengrays.key.pub >> ${HOME}/.ssh/authorized_keys
$ chmod 600 ${HOME}/.ssh/authorized_keys
```

後で使うので、ゲストのIPアドレスを調べておきます。

```shell-session
（ゲストで実行）
$ ip address
```

**ホスト**の`%USERPROFILE%\.ssh\config`にSSH設定ファイルを作成します。調べたIPアドレスをHostNameに指定します。

```txt:%USERPROFILE%\.ssh\config
Host centos7_sandbox
    HostName 192.168.43.119
    User rolengrays
    IdentityFile "~/.ssh/rolengrays.key"
```

ホストからSSH接続してみます。SSH経由でコマンドを正しく実行できることを確認します。

```cmd
（ホストで実行）
c:\>ssh centos7_sandbox cat /etc/redhat-release
CentOS Linux release 7.8.2003 (Core)
```

### 2.6 mDNS設定

2.5節の手順で仕組み上はSSH接続ができるようになりました。しかし、DHCPで各ホストにIPアドレスを動的に割りあてている場合、ゲストのIPアドレスが意図せず変更されることがあります。そうなるとまたゲストのIPアドレスを調べおして、`%USERPROFILE%\.ssh\config`を更新する必要があります。これでは面倒なので、mDNSの仕組みを使って同じネットワーク内のIPアドレスをホスト名で解決できるようにします。

※ Windows 10のバージョンが古いと、mDNSクライアントがデフォルトで入っていない場合があります。

まずはゲストにホスト名を設定します。自分の好きな名前を付けましょう。ここでは`c7sandbox`とします。終わったら、ゲストを再起動しておきます。

```shell-session
（ゲストで実行）
# hostnamectl set-hostname c7sandbox
```

続いて、ゲストにmDNSデーモンをインストールします。

```shell-session
（ゲストで実行）
# yum install avahi
```

avahi-daemonサービスを起動します。自動起動もONにしておきます。

```shell-session
（ゲストで実行）
# systemctl enable avahi-daemon
# systemctl start avahi-daemon
```

5353/UDPがLISTENしていることを確認します。

```shell-session
（ゲストで実行）
# netstat -nulp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
udp        0      0 0.0.0.0:40913           0.0.0.0:*                           1464/avahi-daemon:
udp        0      0 0.0.0.0:68              0.0.0.0:*                           865/dhclient
udp        0      0 0.0.0.0:5353            0.0.0.0:*                           1464/avahi-daemon:
udp        0      0 127.0.0.1:323           0.0.0.0:*                           718/chronyd
udp6       0      0 ::1:323                 :::*                                718/chronyd
```

最後にfirewalldの設定を変更し、外から5353/UDPへの通信を許可するようにします。デフォルトゾーンをhomeにします。

```shell-session
（ゲストで実行）
# firewall-cmd --set-default-zone=home
success
```

homeゾーンが、現在使っているインターフェースに設定されていることを確認します。

```shell-session
（ゲストで実行）
# firewall-cmd --get-active-zones
home
  interfaces: enp0s3
```

※ homeゾーンではmdnsサービスがデフォルトで有効に設定されています。以下のコマンドで確認します。

```shell-session
（ゲストで実行）
# firewall-cmd --zone home --list-services
dhcpv6-client mdns samba-client ssh
```

※ mdnsサービスは5353/UDPを許可しています。

```shell-session
（ゲストで実行）
# firewall-cmd --service mdns --get-ports --permanent
5353/udp
```

firewalldを再起動して設定を反映します。

```shell-session
（ゲストで実行）
# systemctl restart firewalld
```

**ホスト**の、`%USERPROFILE%\.ssh\config`ファイルを書き換えます。ゲストのホスト名が`c7sandbox`なので、末尾に`.local`をつけたものをHostNameに指定します。

※ mDNSはデフォルトで、"ホスト名 + .local"で名前解決するためです。

```txt:%USERPROFILE%\.ssh\config
Host centos7_sandbox
    HostName c7sandbox.local
    User rolengrays
    IdentityFile "~/.ssh/rolengrays.key"
```

ホストからもう一度SSHできることを確認します。

```cmd
（ホストで実行）
c:\>ssh centos7_sandbox hostname
c7sandbox
```

これでIPアドレスではなく、名前でホストからゲストにSSH接続ができました。

## 3. Visual Studio Codeの導入

[Visual Studio Code](https://azure.microsoft.com/ja-jp/products/visual-studio-code/)をインストールします。このテキストエディタには豊富な拡張機能があり、開発を強力にサポートしてくれます。

Remote Development拡張機能をインストールします。これを使うと、ゲストのファイルをホストで簡単に編集できます。

![006_remote001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/8295e943-f385-3197-e2f9-16f04672e187.png)

2.4節、2.5節のSSH設定ファイルをこの拡張機能が認識しているので、すぐにSSH接続できます。

![006_remote002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/c8f959f1-631e-74e1-1034-9df4a40231e2.png)

## 4. 開発環境の構築

不安な場合は仮想マシンのスナップショットを取っておくとよいです。

### 4.1 Gitの最新化

開発環境ではバージョン管理システムが必要となる場面が多いです。Gitを使うことにします。ここまでの作業を実施している場合、既にGitがインストールされていますが、バージョンが古いので最新化します。（4.5節で導入するVisual Studio CodeのGit拡張機能がGitのバージョン2.0以上でうまく動作するため。）

現在のGitのバージョン（CentoOS 7デフォルト）は以下です。

```shell-session
# git --version
git version 1.8.3.1
```

執筆時点の最新版は`2.27.0`です。手順は以下を参考にします。

- [Git - Installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [git/INSTALL at v2.27.0 · git/git · GitHub](https://github.com/git/git/blob/v2.27.0/INSTALL)

ビルド前の準備します。Gitのビルドに必要な以下のパッケージをインストールします。

```shell-session
# yum install \
  zlib-devel \
  perl-devel \
  openssl-devel \
  curl-devel \
  expat-devel \
  gettext-devel
```

ドキュメントのビルドに必要な以下のパッケージをインストールします。（ドキュメントはいらないかもしれませんが...）

```shelll-session
# yum install \
  asciidoc \
  xmlto
```

既にある古いGitでGitそのもののリポジトリをcloneしてきます。

```shell-session
# cd /tmp
# git clone https://github.com/git/git.git
```

最新版をチェックアウトします。

```shell-session
# cd /tmp/git
# git checkout v2.27.0
```

ビルド/インストールします。インストール先は`/usr/local`にします。少し時間がかかるので気長に待ちましょう。

```shell-session
# make configure
# ./configure --prefix=/usr/local
# make all doc
# make install install-doc install-html
```

hashをリセットしたあと、新しいGitのバージョンを確認します。

```shell-session
# hash -r
# git --version
git version 2.27.0
```

### 4.2 Dockerのインストール

Docker Engineをインストールします。Dockerによって、プロセスを隔離した環境で実行できます。以下を参考にします。

- [Get Docker Engine - Community for CentOS | Docker Documentation](https://docs.docker.com/install/linux/docker-ce/centos/)

必要なパッケージをインストールします。

```shell-session
# yum install \
  yum-utils \
  device-mapper-persistent-data \
  lvm2
```

リポジトリを有効化します。

```shell-session
# yum-config-manager \
  --add-repo \
  https://download.docker.com/linux/centos/docker-ce.repo
```

インストールします。

```shell-session
# yum install \
  docker-ce \
  docker-ce-cli \
  containerd.io
```

dockerサービスの自動起動を有効化、起動します。

```shell-session
# systemctl enable docker
# systemctl start docker
```

動作確認します。

```shell-session
# docker run hello-world
```

#### 4.2.1 dockerグループへの追加

rolengraysユーザーをdockerグループに追加します。これでsudoなしでrolengraysユーザーがdockerコマンドを実行できます。

```shell-session
# usermod -aG docker rolengrays
```

※ ユーザーdockerグループに追加すると、Dockerホストでそのユーザーにroot権限を与えることとほぼ同じとなります。

### 4.3 Python3のインストール

Python3をインストールします。あとでインストールするdocker-composeの`1.25.x`以降のバージョンがPython2をサポートしないためです。（そしてPython2は既に公式のサポートが切れています。）CentOS 7はデフォルトだとPython2しかインストールされていません。

以下を参考にインストールします。

- [CentOS 環境のPython - python.jp](https://www.python.jp/install/centos/index.html)

必要なパッケージをインストールします。

```shell-session
# yum groupinstall "development tools"
# yum install \
  bzip2-devel \
  gdbm-devel \
  libffi-devel \
  libuuid-devel \
  ncurses-devel \
  openssl-devel \
  readline-devel \
  sqlite-devel \
  tk-devel \
  wget \
  xz-devel \
  zlib-devel
```

ソースコードをダウンロードします。

```shell-session
# cd /tmp
# wget https://www.python.org/ftp/python/3.8.3/Python-3.8.3.tgz
# tar -xzf Python-3.8.3.tgz
```

ビルド・インストールします。

```shell-session
# cd /tmp/Python-3.8.3
# ./configure --enable-shared
# make
# make install
# sh -c "echo '/usr/local/lib' > /etc/ld.so.conf.d/custom_python3.conf"
# ldconfig
```

バージョンを確認します。

```shell-session
# python3 --version
Python 3.8.3
```

### 4.4 docker-composeのインストール

docker-composeは同時に複数のコンテナを使うアプリケーションを定義・実行できるツールです。コンテナの実行はdockerコマンドでも実施できますが、オプションが長いコマンドを複数回打つのは非常に面倒です。docker-composeはこの問題を解決できます。以下の通りインストールします。

- [Install Docker Compose | Docker Documentation](https://docs.docker.com/compose/install/)

執筆時点の最新版は`1.26.0`です。

```shell-session
# curl -L "https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose
```

```shell-session
# docker-compose --version
docker-compose version 1.26.0, build d4451659
```

### 4.5 Visual Studio Code拡張機能のインストール

以下の拡張機能をインストールしておきます。細かい機能は紹介しませんが、便利です。実際にインストールして機能を確かめてみてください。

- GitLens（行ごとのBlameができる、ファイルの編集履歴をGUIで確認できる、など）
- Docker（実行中のコンテナや手元にあるイメージなどをGUIで表示してくれる、など）

---

ここまでで、環境構築は終わました。

## 5. デモ

利用例を紹介します。

### 5.1 最低限のWebサイトを構築してみる

最低限のWebサイトをDockerコンテナで構築してみます。最終的にカレントディレクトリ配下は以下のようになります。

```txt
.
├── c7-base
│   └── Dockerfile
├── c7-systemd
│   └── Dockerfile
└── docker-compose.yml
```

#### 5.1.1 systemd有効コンテナイメージの作成

systemdを有効にしたコンテナの定義を作成します。`./c7-systemd/Dockerfile`を以下のようにします。[centos - Docker Hub](https://hub.docker.com/_/centos)からDockerfileを引用します。

```Dockerfile:./c7-systemd/Dockerfile
# https://hub.docker.com/_/centos より引用
FROM centos:7
ENV container docker
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*;\
rm -f /etc/systemd/system/*.wants/*;\
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*;\
rm -f /lib/systemd/system/anaconda.target.wants/*;
VOLUME [ "/sys/fs/cgroup" ]
CMD ["/usr/sbin/init"]
```

c7-systemdのコンテナイメージをビルドします。

```shell-session
$ docker build -t local/c7-systemd ./c7-systemd
```

#### 5.1.2 自分の基本イメージの作成

基本イメージを作成します。目的にかかわらず、自分がよく使う汎用パッケージや設定などをあらかじめコンテナイメージにしておくと便利です。`./c7-base/Dockerfile`を以下のようにします。先ほどビルドした`local/c7-systemd`をベースにした上で、よく使うパッケージを自分で適当に選んでインストールするとよいです。デフォルトだとタイムゾーンがUTCなので、JSTに設定しておきます。

```Dockerfile:./c7-base/Dockerfile
FROM local/c7-systemd

# アップデート＆パッケージインストール
RUN yum update -y \
  && yum install -y \
    wget \
    tree \
    which \
    net-tools

# タイムゾーン設定
RUN /usr/bin/ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
```

こちらもビルドします。

```shell-session
$ docker build -t local/c7-base ./c7-base
```

#### 5.1.3 docker-compose.ymlの作成

docker-compose.ymlを作成します。通常、Dockerコンテナを実行するためには、場合によっては長いコマンドラインオプションを複数打たなければならず、面倒です。`./docker-compose.yml`を以下のようにします。コンテナ1つだけ定義します。

```yaml:./docker-compose.yml
---
version: "3.7"
services:
  c7-base:
    build:
      context: ./c7-base
    image: local/c7-base
    container_name: c7-base
    working_dir: /root
    privileged: true # systemdをコンテナ内で使用するための設定
    stdin_open: true
    tty: true
    # Dockerホストの'8080/TCP'をコンテナの'80/TCP'にマッピングするため
    ports:
      - published: 8080
        target: 80
        protocol: tcp
        mode: host
    volumes:
      # systemdをコンテナ内で使用するための設定
      - type: bind
        source: /sys/fs/cgroup
        target: /sys/fs/cgroup
        read_only: true
```

※ 上記のdocker-compose定義でのコンテナ実行は、以下のコマンドとほぼ同等です。

```shell-session
# docker run -it --privileged -w /root -p 8080:80 -v /sys/fs/cgroup:/sys/fs/cgroup:ro --name c7-base local/c7-base
```

#### 5.1.4 コンテナ内でのWebサーバーの作成作業

docker-composeでdockerコンテナを起動します。

```shell-session
$ docker-compose -f ./docker-compose.yml up -d
```

dockerコンテナにシェルをアタッチします。Visual Studio CodeのDocker拡張機能を使えば、コマンドなしでできます（コンテナを右クリック->Attach Shell）。

```shell-session
[rolengrays@c7sandbox ~]$ docker exec -it c7-base /bin/bash
[root@0b7b64ad6eb6 ~]# 
```

これでコンテナ内でコマンドを実行できるようになりました。試しにhttpdをインストールして、コンテナ内にWebサーバーを作ってみます。内容が"Hello From Container"のみの簡単なHTMLドキュメントも用意しておきます。

```shell-session
（コンテナ内で実行）
# yum install httpd
# echo "Hello From Container" > /var/www/html/index.html
# systemctl start httpd
```

終わったら`exit`コマンドで抜けます。

#### 5.1.5 コンテナ内のWebサーバーにアクセス

ホストからこのWebサーバーにアクセスしてみます。ゲストの'8080/TCP'をコンテナの'80/TCP'にマッピングするように設定しているため、firewalldに8080/TCPを許可する設定をする必要があります。

まずはサービスを作成します。名前は`http_8080`とします。以下のようなXMLファイルを作成すればOKです。

```xml:/usr/lib/firewalld/services/http_8080.xml
<?xml version="1.0" encoding="utf-8"?>
<service>
  <short>HTTP(8080)</short>
  <description>HTTP 8080 Port</description>
  <port protocol="tcp" port="8080"/>
</service>
```

ファイルを作成したら、リロードします。

```shell-session
（ゲストで実行）
# firewall-cmd --reload
```

次にhomeゾーンに先ほど作成した`http_8080`を追加します。追加したら、もう一度リロードします。

```shell-session
（ゲストで実行）
# firewall-cmd --permanent --zone home --add-service http_8080
# firewall-cmd --reload
```

これで8080/TCPに外からアクセスできるようになりました。ホストでWebブラウザを立ち上げて、`http://c7sandbox.local:8080/index.html`にアクセスしてみましょう。`Hello From Container`が表示されれば成功です。

![007_demo001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/97932bee-fcab-ca4f-f883-77a4df9a65c4.png)

Webサーバーのインストールを最初からやり直したい場合は、以下のように一度downしてから、再度upすることで環境（コンテナ）を再構築できます。

```shell-session
$ docker-compose down
$ docker-compose up -d
```

これでインストールからやり直すことができます。今回はhttpdをインストールするだけの例ですが、代わりにnginxをインストールしてみたりと、ほかにもいろいろな実験を行うことができます。もう一度シェルをアタッチしてアクセスログを見たりもできます。

### 5.2 Apache Web Server + Tomcatでデモ用Webアプリを作ってみる

複数のコンテナを使用するパターンで試してみます。Apache Web ServerのコンテナとTomcatのコンテナを作成し、それらを連携させてみます。プロジェクトのディレクトリ構成は最終的に以下のようになります。

```txt
demo52
├── app
│   ├── Dockerfile
│   ├── build.sh
│   ├── conf
│   │   └── server.xml
│   ├── resource
│   │   ├── apache-tomcat-9.0.36.tar.gz
│   │   └── openjdk-14.0.1_linux-x64_bin.tar.gz
│   └── testapp
│       ├── pom.xml
│       （略）
├── docker-compose.yml
└── proxy
    ├── Dockerfile
    └── conf
        └── app.conf
```

以下に簡単なイメージを載せておきます。

![009_ajp001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/190b736a-66f1-fcd9-8ec5-ff83b86b1cba.png)

#### 5.2.1 テスト用Webアプリの作成

Tomcatの上で動かす簡単なアプリを作ります。ビルドツールはMavenを使います。公式のDockerイメージを使いましょう。

まずはDockerホスト側にMavenローカルリポジトリを作成します。これをコンテナ内に置いてしまうと、コマンド実行後にダウンロードしたパッケージがコンテナごと破棄されてしまいます。こうなると2度目のビルド以降も大量のダウンロードが発生するため、時間の無駄になります。コンテナ実行後も永続化したいファイルはコンテナ外（ここではDockerホスト）に配備するのがよいです。

```shell-session
$ mkdir -p ${HOME}/data/maven_repo
```

以下のようにディレクトリを作成しておきます。

```shell-session
# mkdir -p ./app/conf ./app/resource ./proxy/conf
```

次にMavenでWebアプリプロジェクトのひな形を作ります。公式からMavenイメージをpullしてから、コンテナを作成・実行して必要なパッケージのダウンロードなどを行うため、少し時間がかかります。

```shell-session
$ docker run \
  -it \
  --rm \
  --name my-maven-project \
  -v $(pwd)/app:/usr/src/mymaven \
  -v ${HOME}/data/maven_repo:/root/.m2 \
  -w /usr/src/mymaven \
  maven:3.6.3-jdk-14 mvn archetype:generate \
    -DinteractiveMode=false \
    -DarchetypeGroupId=org.apache.maven.archetypes \
    -DarchetypeArtifactId=maven-archetype-webapp \
    -DarchetypeVersion=1.4 \
    -DgroupId=com.rolengrays.example \
    -DartifactId=testapp \
    -Dversion=1.0.0
```

終わったら、`./app/testapp`というディレクトリができているはずです。

```txt
./app/testapp
├── pom.xml
└── src
    └── main
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            └── index.jsp
```

特にユーザーを指定せずにDockerコンテナからファイルを作成したので、所有者がrootユーザーになっています。自分のユーザーにしましょう。

```shell-session
$ sudo chown -R rolengrays:rolengrays ./app/testapp
```

次に`pom.xml`を編集します。今回はJDK14を使いたいので、デフォルトから以下のように書き換えます。

```diff
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-   <maven.compiler.source>1.7</maven.compiler.source>
-   <maven.compiler.target>1.7</maven.compiler.target>
+   <maven.compiler.source>1.14</maven.compiler.source>
+   <maven.compiler.target>1.14</maven.compiler.target>
  </properties>
```

```diff
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
+         <configuration>
+           <source>14</source>
+           <target>14</target>
+         </configuration>
        </plugin>
```

各種ソースファイルを準備します。

```shell-session
$ mkdir -p ./app/testapp/src/main/java/com/rolengrays/example
```

`Hello.java`では、単純な文字列を生成するだけのクラスを定義します。

```java:./app/testapp/src/main/java/com/rolengrays/example/Hello.java
package com.rolengrays.example;

public class Hello {
  public Hello() {}
  public static String greetings() { return new String("Hello From Container Web App"); }
}
```

`index.jsp`では、Helloクラスに文字列を出力させます。

```jsp:./app/testapp/src/main/webapp/index.jsp
<%@ page import="com.rolengrays.example.Hello" %>
<html>
<body>
<h2>
  <% out.println(Hello.greetings()); %>
</h2>
</body>
</html>
```

ソースの準備が終わったので、ビルドしましょう。コマンドが長いため、ビルドスクリプトを作っておきます。

```bash:./app/build.sh
#!/bin/bash
docker run \
  --rm \
  --name my-maven-project \
  -v "$(pwd)/app/testapp":/usr/src/mymaven \
  -v ${HOME}/data/maven_repo:/root/.m2 \
  -w /usr/src/mymaven \
  maven:3.6.3-jdk-14 mvn package
```

```shell-session
$ chmod +x ./app/build.sh
$ ./app/build.sh
```

※ [multi-stage build](https://docs.docker.com/develop/develop-images/multistage-build/)で、コンテナイメージのビルド時にWebアプリのビルドを行い、成果物（warファイル）だけをコンテナイメージに残す方法があります。しかし、コンテナイメージのビルド時にMavenのローカルリポジトリを参照するうまい方法がないため、ビルドのたびに大量のダウンロードが発生します。それを避けるため、今回はWebアプリのビルドをコンテナイメージのビルド前にいちいち手動で行うこととします。

#### 5.2.2 Tomcatのコンテナイメージの作成

必要なソフトウェアをダウンロードします。

| ソフトウェア | ダウンロードページ | 保存先 |
| --- | --- | --- |
| OpenJDK 14 | https://jdk.java.net/14/ | ./app/resource/openjdk-14.0.1_linux-x64_bin.tar.gz |
| Tomcat 9 | https://tomcat.apache.org/download-90.cgi | ./app/resource/apache-tomcat-9.0.36.tar.gz |

```shell-session
$ ls -l ./app/resource
合計 204952
-rw-rw-r--. 1 rolengrays rolengrays  11200905  6月  4 03:03 apache-tomcat-9.0.36.tar.gz
-rw-rw-r--. 1 rolengrays rolengrays 198665889  3月  5 19:40 openjdk-14.0.1_linux-x64_bin.tar.gz
```

TomcatがApache Web ServerとAJP通信できるように設定をします。Tomcat 9のアーカイブから`server.xml`を抜き出して、`./app/conf`に配備します。

```shell-session
$ tar -C /tmp -xzf ./app/resource/apache-tomcat-9.0.36.tar.gz apache-tomcat-9.0.36/conf/server.xml
$ cp /tmp/apache-tomcat-9.0.36/conf/server.xml ./app/conf
$ rm -rf /tmp/apache-tomcat-9.0.36
```

その後、`./app/conf/server.xml`のAJP設定部分を以下のように編集します。（デフォルトだとコメントアウトされています。）

```xml:./app/conf/server.xml
    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector protocol="AJP/1.3"
               secretRequired="false"
               address="0.0.0.0"
               port="8009"
               redirectPort="8443" />
```

- `secretRequired="false"`属性を追加します。ないとエラーになります。`true`にした場合はほかにもいろいろ設定が必要で手間がかかるため、ここでは明示的に`false`を設定します。
- `address`属性値を`0.0.0.0`に書き換えます。デフォルトは`address`が`::1`でIPv6になっていますが、今回はDockerコンテナ・ネットワークをIPv6で構成していないため、IPv4でもListenできるように`0.0.0.0`に書き換えます。

`./app/Dockerfile`を作成します。

```Dockerfile:./app/Dockerfile
# 5.1節で作成した自分の基本イメージをベースとする
FROM local/c7-base

# 変数
ARG java_home="/usr/local/java/jdk-14.0.1"
ARG catalina_home="/usr/local/tomcat/apache-tomcat-9.0.36"

# JDK14のインストール
ADD ./resource/openjdk-14.0.1_linux-x64_bin.tar.gz /usr/local/java
ENV JAVA_HOME ${java_home}

# Tomcat9のインストール
ADD ./resource/apache-tomcat-9.0.36.tar.gz /usr/local/tomcat
ENV CATALINA_BASE ${catalina_home}
ENV CATALINA_HOME ${catalina_home}
## AJPの設定を記述したserver.xmlをコピー
COPY ./conf/server.xml ${catalina_home}/conf/server.xml

# ビルドしたwarの配備
COPY ./testapp/target/testapp.war ${catalina_home}/webapps

CMD [ "/bin/bash", "-c", "${CATALINA_HOME}/bin/catalina.sh run" ]
```

#### 5.2.3 AJPプロキシのコンテナイメージの作成

Apache Web ServerをAJPプロキシサーバーとして構成します。以下のVirtualHost定義を作成します。これでプロキシサーバーへのアクセスをWebアプリサーバーの`/testapp/`パスへ転送します。

```conf:./app/app.conf
<VirtualHost *:80>
  ServerName c7sandbox.local
  ProxyPass / ajp://app:8009/testapp/
</VirtualHost>
```

```Dockerfile:./proxy/Dockerfile
FROM local/c7-systemd

RUN yum install -y httpd && systemctl enable httpd
COPY ./conf/app.conf /etc/httpd/conf.d/app.conf
```

#### 5.2.4 Webアプリの構築

docker-composeの定義を作成します。以下の通り、2種類のコンテナを定義します。

```yaml:./docker-compose.yml
---
version: "3.7"
services:
  # AJPプロキシサーバーのコンテナ
  proxy:
    build:
      context: ./proxy
    image: local/proxy
    networks:
      - testnet
    privileged: true
    stdin_open: true
    tty: true
    ports:
      - target: 80
        published: 8080
        protocol: tcp
        mode: host
    volumes:
      - type: bind
        source: /sys/fs/cgroup
        target: /sys/fs/cgroup
        read_only: true
  # Webアプリサーバーのコンテナ
  app:
    build:
      context: ./app
    image: local/app
    container_name: app # プロキシサーバーからappで名前解決できるようになります。
    networks:
      - testnet
    privileged: true
    stdin_open: true
    tty: true
    volumes:
      - type: bind
        source: /sys/fs/cgroup
        target: /sys/fs/cgroup
        read_only: true
networks:
  testnet:
```

#### 5.2.5 コンテナの実行

コンテナを実行します。`--build`でコンテナイメージをビルドしてから実行します。

```shell-session
$ docker-compose up -d --build
```

※ 5.1節で起動したコンテナが残っていたら、上記を実行する前に停止してください。

ホストOSでブラウザから`http://c7sandbox.local:8080/`にアクセスしてみましょう。以下のように`Hello From Container Web App`が表示されれば成功しています。

![008_demo001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279459/5023a13a-405f-f34d-400c-625dd6fba0e6.png)

設定変更などでいろいろ実験したい場合は、Dockerコンテナ内にシェルをアタッチして環境を手動で変更できます。もちろん、コマンドの代わりにVisual Studio CodeのDocker拡張機能を使ってアタッチしてもよいです。

```shell-session
$ docker ps -a
CONTAINER ID  IMAGE        COMMAND                 CREATED        STATUS        PORTS                 NAMES
08d677a1ce69  local/proxy  "/usr/sbin/init"        2 minutes ago  Up 2 minutes  0.0.0.0:8080->80/tcp  demo52_proxy_1
edc3f676c012  local/app    "/bin/bash -c '${CAT…"  2 minutes ago  Up 2 minutes                        app
```

プロキシサーバーのコンテナ名を`docker-compose.yml`で指定していないため、docker-composeが自動で命名しています。

```shell-session
$ docker exec -it demo52_proxy_1 /bin/bash
```

```shell-session
$ docker exec -it app /bin/bash
```

初期状態にコンテナを戻したい場合は、5.1節でやったように以下のコマンドを実行します。Dockerfileを編集した場合は、`--build`オプションをつけてupしましょう。

```shell-session
$ docker-compose down
$ docker-compose up -d --build
```

## 6. まとめ

今回は、VirtualBoxとDockerで開発環境を構築してみました。Dockerで環境を隔離できて作り直しも楽なので、いろんなことを気軽に実験できます。