Java, MySQL, Redisを利用するWebアプリケーションの開発環境をDocker CE for Windowsで構築する

# 概要

MySQL、Redisを利用するWebアプリケーションをJava（フレームワークにSpring Bootを使用）で開発、実行する環境をDocker CE for Windowsで構築したときのメモです。
MySQLおよびRedisはオフィシャルイメージを利用したコンテナで、Spring Bootで開発したWebアプリケーションはCentOS7のオフィシャルイメージを利用したコンテナにOpenJDKをインストールして実行します。

ソースコードは[rubytomato/docker-demo-redis] (https://github.com/rubytomato/docker-demo-redis)にあります。

**環境**

* Windows 10 Professional
* Docker CE for Windows 18.06.0
    * Redis 4.0.11
    * MySQL 8.0.12
    * CentOS 7
        * OpenJDK 1.8.0_181
        * Spring Boot 2.0.4

# MySQL,Redis,Webアプリケーションの実行環境を構築

## プロジェクトディレクトリの構成

コンテナの管理、実行はdocker-composeで行うので、プロジェクトのルートディレクトリにdocker-compose.ymlを配置しています。それぞれのコンテナの情報はサブディレクトリ毎に分離しています。

project-root以下には大きく分けて4つのサブディレクトリがあります。
demo-redis-springというサブディレクトリは、コンテナではなくapp-serverで実行するWebアプリケーションのプロジェクトディレクトリです。

|sub directory     |description |
|:-----------------|:-----------|
|app-server        |Webアプリケーションを実行するコンテナのDockerfileと関連ファイルを格納するディレクトリです。 |
|mysql-server      |mysql-serverを実行するコンテナのDockerfileと関連ファイルを格納するディレクトリです |
|redis-server      |redis-serverを実行するコンテナのDockerfileと関連ファイルを格納するディレクトリです。 |
|demo-redis-spring |Spring Bootを利用したWebアプリケーションのプロジェクトディレクトリです。 |

**ディレクトリ構成**

```text
/project-root
 |
 +--- docker-compose.yml
 +--- README.md
 |
 +--- /demo-redis-spring
 |      |
 |      +--- pom.xml
 |      |
 |      +--- /src
 |      |      |
 |      |      +--- /main
 |      |      +--- /test
 |      |
 |      +--- /target
 |             |
 |             +--- demo.jar
 |
 +--- /app-server
 |      |
 |      +--- Dockerfile
 |      |
 |      +--- start.sh
 |
 +--- /mysql-server
 |      |
 |      +--- Dockerfile
 |      |
 |      +--- /conf
 |      |      |
 |      |      +--- mysqld.cnf
 |      |
 |      +--- /sql
 |             |
 |             +--- 1_schema.sql
 |             +--- 2_init_memo_data.sql
 |
 +--- /redis-server
        |
        +--- Dockerfile
        |
        +--- /conf
               |
               +--- redis.conf
```

## MySQLサーバー用コンテナ

イメージはMySQLのオフィシャルを利用します。

* [library/mysql - Docker Hub] (https://hub.docker.com/_/mysql/)

### Dockerfile

```text
FROM mysql:8.0.12

COPY ./conf/mysqld.cnf /etc/mysql/conf.d

COPY ./sql/1_schema.sql /docker-entrypoint-initdb.d
COPY ./sql/2_init_memo_data.sql /docker-entrypoint-initdb.d
```

### Configuration

/conf/mysqld.cnfの内容は以下の通りです。このファイルをコンテナの/etc/mysql/conf.dへコピーするとMySQL起動時に読み取られて反映されます。

```text
[mysqld]
character_set_server = utf8mb4
collation_server = utf8mb4_general_ci

log_output = FILE
general_log = 1
log_queries_not_using_indexes = 1
log_slow_admin_statements = 1
log_syslog = 0
log_timestamps = SYSTEM
long_query_time = 3
slow_query_log = 1

general_log_file = general_query_all.log
log_error = error.log
slow_query_log_file = slow_query.log

log_bin_trust_function_creators = 1

[mysql]
show-warnings

prompt = "\u@\h [\d] > "
```

### SQL Script

SQL Scriptをコンテナの/docker-entrypoint-initdb.dへコピーすると、データベースの初期化時に実行されます。

**1_schema.sql**

```sql
CREATE DATABASE sample_db DEFAULT CHARACTER SET = utf8mb4;

CREATE USER 'test_user'@'%' IDENTIFIED BY 'test_user' PASSWORD EXPIRE NEVER;

GRANT ALL ON sample_db.* TO 'test_user'@'%';

USE sample_db;

CREATE TABLE IF NOT EXISTS memo (
   id BIGINT AUTO_INCREMENT,
   title VARCHAR(255) NOT NULL,
   description TEXT NOT NULL,
   done BOOLEAN DEFAULT FALSE NOT NULL,
   updated TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL,
   PRIMARY KEY (id)
)
CHARACTER SET = utf8mb4,
COLLATE utf8mb4_general_ci;
```

**2_init_memo_data.sql**

```sql
USE sample_db;

START TRANSACTION;

INSERT INTO memo (title, description, done, updated) VALUES ('title A', 'description A', false, '2018-08-01');
INSERT INTO memo (title, description, done, updated) VALUES ('title B', 'description B', false, '2018-08-02');

COMMIT;
```

## Redisサーバー用コンテナ

イメージはRedisのオフィシャルを利用します。

* [library/redis - Docker Hub] (https://hub.docker.com/_/redis/)

ちなみに、Windows上でRedisサーバーを稼働させる場合、Redis 3.0までは[MicrosoftArchive/redis] (https://github.com/MicrosoftArchive/redis)からビルドファイルが入手できましたが、4.0以上は提供されていない為、実質的にDockerなどの仮想環境を利用することになります。

### Dockerfile

```text
FROM redis:4.0.11

COPY ./conf/redis.conf /usr/local/etc/redis/redis.conf

RUN set -x && \
    touch /var/log/redis.log && \
    chown redis:root /var/log/redis.log && \
    chmod 664 /var/log/redis.log

CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]
```

### Configuration

/conf/redis.confは、[https://raw.githubusercontent.com/antirez/redis/4.0/redis.conf] (https://raw.githubusercontent.com/antirez/redis/4.0/redis.conf)をベースに下記の部分を変更しました。
このファイルをコンテナの/usr/local/etc/redis/redis.confへコピーし、redis-server起動時に引数として与えます。

**変更前の箇所**

```text
bind 127.0.0.1
logfile ""
# maxclients 10000
# requirepass foobared
appendonly no
```

**変更後の箇所**

```text
# bind 127.0.0.1
logfile /var/log/redis.log
maxclients 100
requirepass foobared
appendonly yes
```

#### logfile

この例では、ログをコンテナ内の/var/log/redis.logへ出力するように変えていますが、logfileの設定をデフォルトにしておくと標準出力(stdout)へ出力されます。標準出力に出力されるログはdocker-compose logsで確認することができます。

```text
> docker-compose logs -f redis-server
```

#### redis-cliのコマンドを監視したい場合

他のクライアントから実行されたコマンドを監視したい場合はredis-cli monitorを使用します。

```text
> docker-compose exec redis-server redis-cli
127.0.0.1:6379> auth foobared
OK
127.0.0.1:6379> monitor
OK
```

#### requirepass

bindをコメントアウトすると、redisコマンドを実行する前にauthで認証する必要があります。
このときのauthで指定するパスワードをrequirepassで設定します。

## Applicationサーバー用コンテナ

ApplicationサーバーではSpring Bootで開発したWebアプリケーションを実行します。
利用するイメージはOpenJDKのオフィシャルイメージではなくCentOS7のオフィシャルイメージで、それにyumでOpenJDK 1.8をインストールしました。

* [library/centos - Docker Hub] (https://hub.docker.com/_/centos/)

### Dockerfile

ロケールの変更とWebアプリケーションの実行ユーザーとしてappというユーザーを追加しています。

```text
FROM centos:7

RUN set -x && \
    yum update -y && \
    yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel && \
    yum reinstall -y glibc-common && \
    yum -y clean all

ENV JAVA_HOME /usr/lib/jvm/java-1.8.0-openjdk

# locale settings
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja

RUN set -x && \
    localedef -i ja_JP -c -f UTF-8 ja_JP.UTF-8 && \
    unlink /etc/localtime && \
    ln -s /usr/share/zoneinfo/Japan /etc/localtime

# application user settings
ENV HOME /home/app

RUN set -x && \
    groupadd app && \
    useradd -g app -d /home/app -s /bin/bash app

WORKDIR $HOME
USER app

COPY start.sh .

EXPOSE 9000
```

### start.sh

Webアプリケーションを簡単に実行するためのshellです。
コンテナの/var/targetは、Windows上の/project-root/demo-redis-spring/targetをマウントしたディレクトリです。
このマウントはdocker-compose.ymlの方で記述しています。

```bash
#!/bin/bash

set -x

java -Xmx512m -Xms512m -XX:MaxMetaspaceSize=256m -verbose:gc -Xloggc:app-gc.log -jar /var/target/demo.jar
```

### app-serverコンテナ起動時にwebアプリケーションを実行する

この記事では、app-serverコンテナの起動とそのコンテナ内で実行するwebアプリケーションの実行はタイミングを分けていますが、app-serverコンテナ起動とともにwebアプリケーションを実行するには、Dockerfileの最後に下記の行を追加します。

```text
CMD [ "./start.sh" ]
```

webアプリケーションのログは、docker-compose logsコマンドで確認できます。

```text
> docker-compose logs -f --tail=all app-server
Attaching to app-server
app-server      | + java -Xmx512m -Xms512m -XX:MaxMetaspaceSize=256m -verbose:gc -Xloggc:app-gc.log -jar /var/target/demo.jar
app-server      |
app-server      |   .   ____          _            __ _ _
app-server      |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
app-server      | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
app-server      |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
app-server      |   '  |____| .__|_| |_|_| |_\__, | / / / /
app-server      |  =========|_|==============|___/=/_/_/_/
app-server      |  :: Spring Boot ::        (v2.0.4.RELEASE)

// ...省略...
```


## docker-compose.yml

```yml
version: "3.6"
services:

  mysql-server:
    container_name: mysql-server
    build:
      context: ./mysql-server
    volumes:
      - mysql-data:/var/lib/mysql
    environment:
      TZ: Asia/Tokyo
      MYSQL_ROOT_PASSWORD: password
    ports:
      - 3306:3306
    networks:
      - redis-network
    restart: always
    tty: true

  redis-server:
    container_name: redis-server
    build:
      context: ./redis-server
    environment:
      TZ: Asia/Tokyo
    volumes:
      - redis-data:/data
    ports:
      - 6379:6379
    networks:
      - redis-network
    restart: always
    tty: true

  app-server:
    build:
      context: ./app-server
    environment:
      TZ: Asia/Tokyo
      SPRING_PROFILES_ACTIVE: docker
    volumes:
      - ./demo-redis-spring/target:/var/target
    depends_on:
      - mysql-server
      - redis-server
    ports:
      - 9000:9000
    networks:
      - redis-network
    restart: always
    stdin_open: true
    tty: true

networks:
  redis-network:
    driver: bridge

volumes:
  mysql-data:
    driver: local
  redis-data:
    driver: local
```

### Spring Profileについて

app-serverコンテナ内からSpring Bootアプリケーションを実行するときに、dockerというプロファイルを指定するためにSPRING_PROFILES_ACTIVEという環境変数を設定します。

```text
app-server:
  environment:
    SPRING_PROFILES_ACTIVE: docker
```

指定されたプロファイルでMySQL serverとRedis serverの接続先をコンテナ内へ切り替えます。
下記はSpring Bootの設定ファイルの関係部分の抜粋です。dockerというプロファイルのときに、MySQL serverとRedis serverのホスト名をコンテナのホスト名にしています。

```yaml:application.yml
spring:
  profiles:
    active: dev

  datasource:
    url: jdbc:mysql://localhost:3306/sample_db?useSSL=false&allowPublicKeyRetrieval=true
    username: test_user
    password: test_user

  redis:
    host: localhost
    port: 6379
    ssl: false
    database: 0
    password: foobared

server:
  port: 9001

---

spring:
  profiles: docker
  datasource:
    url: jdbc:mysql://mysql-server:3306/sample_db?useSSL=false&allowPublicKeyRetrieval=true
  redis:
    host: redis-server

server:
  port: 9000
```

### portsについて

下記のようにmysql-server、redis-serverコンテナのポートを外部に公開しているのは、Windows上でWebアプリケーションを実行するときに、コンテナのMysql server、Redis serverへアクセスできるようにするためです。

mysql-server

```text
ports:
  - 3306:3306
```

redis-server

```text
ports:
  - 6379:6379
```

### コンテナのビルドと起動,停止

**ビルド**

```text
> docker-compose build
```

キャッシュを利用しない場合

```text
> docker-compose build --no-cache
```

**起動**

初回起動時はvolumeが作成されます。

```text
> docker-compose up -d
Creating network "docker-demo-redis_redis-network" with driver "bridge"
Creating volume "docker-demo-redis_mysql-data" with local driver
Creating volume "docker-demo-redis_redis-data" with local driver
Creating mysql-server ... done
Creating redis-server ... done
Creating app-server   ... done
```

次回以降はvolumeの作成はスキップされます。

```text
> docker-compose up -d
Creating network "docker-demo-redis_redis-network" with driver "bridge"
Creating mysql-server ... done
Creating redis-server ... done
Creating app-server   ... done
```

**再起動**

```text
> docker-compose restart
Restarting app-server   ... done
Restarting redis-server ... done
Restarting mysql-server ... done
```

**停止**

```text
> docker-compose down
Stopping app-server   ... done
Stopping mysql-server ... done
Stopping redis-server ... done
Removing app-server   ... done
Removing mysql-server ... done
Removing redis-server ... done
Removing network docker-demo-redis_redis-network
```

### targetディレクトリのマウント

Spring Bootで開発したWebアプリケーションをapp-serverコンテナ上で実行するために、ビルドされたjarファイルが出力されるtargetディレクトリをapp-serverコンテナの/var/targetへマウントします。

```text
  app-server:
    volumes:
      - ./demo-redis-spring/target:/var/target
```

### Webアプリケーションの実行

Webアプリケーションを実行するにはexecコマンドでapp-serverコンテナに入り、コンテナへcopyしておいたstart.shを実行します。

```text
> docker-compose exec app-server bash --login
[app@72e38b38c6ca ~]$ ./start.sh
+ java -Xmx512m -Xms512m -XX:MaxMetaspaceSize=256m -verbose:gc -Xloggc:app-gc.log -jar /var/target/demo.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.4.RELEASE)

2018-08-08 22:09:10.248  INFO 46 --- [           main] com.example.demo.Application             : Starting Application v0.0.1-SNAPSHOT on 72e38b38c6ca with PID 46 (/var/target/demo.jar started by app in /home/app)
2018-08-08 22:09:10.253 DEBUG 46 --- [           main] com.example.demo.Application             : Running with Spring Boot v2.0.4.RELEASE, Spring v5.0.8.RELEASE
2018-08-08 22:09:10.256  INFO 46 --- [           main] com.example.demo.Application             : The following profiles are active: docker

// ...以下省略...
```

app-serverコンテナのポート9000が外部に公開されているので、ブラウザから`http://localhost:9000/`にアクセスしてページが表示されることを確認します。

### MySQL,Redisのデータ永続化

named volumeを利用します。

```text
volumes:
  mysql-data:
    driver: local
  redis-data:
    driver: local
```

データの初期化を行いたい場合は、docker volumeコマンドで削除します。

**volumeの確認**

```text
> docker volume ls
DRIVER              VOLUME NAME
local               docker-demo-redis_mysql-data
local               docker-demo-redis_redis-data
```

**volumeの削除**

```text
> docker volume rm docker-demo-redis_redis-data
docker-demo-redis_redis-data
```

削除すると次回起動時に、自動的に作成されます。

```text
> docker-compose up -d
Creating network "docker-demo-redis_redis-network" with driver "bridge"
Creating volume "docker-demo-redis_redis-data" with local driver
Creating mysql-server ... done
Creating redis-server ... done
Creating app-server   ... done
```

### MySQLサーバーへmysql cliから接続する

MySQLサーバーに接続して作業を行いたい場合は、mysql-serverコンテナからmysqlコマンドで接続します。

```text
> docker-compose exec mysql-server mysql -u test_user -p --database=sample_db
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.12 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

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.

test_user@localhost [sample_db] >
```

### Redisサーバーへredis-cliから接続する

Redisサーバーに接続して作業を行いたい場合は、redis-serverコンテナからredis-cliコマンドで接続します。

```text
> docker-compose exec redis-server redis-cli
127.0.0.1:6379> auth foobared
OK
127.0.0.1:6379> keys *
(empty list or set)
```


# Applicationサーバーのスケールアウト

任意の指定する数だけApplicationサーバーを起動する方法です。

## docker-compose.ymlの修正

portsを下記のように、外部に公開する側のportを範囲で設定します。
この例ではスケールアウトする最大数を2としたいのでportの範囲は9000,9001を指定しました。

```text
  app-server:
    ports:
#      - 9000:9000
      - 9000-9001:9000
```

## 起動

up時に`--scale app-server=num`で、起動するapp-serverの数をnumに指定します。

```text
> docker-compose up -d --scale app-server=2
Creating network "docker-demo-redis_redis-network" with driver "bridge"
Creating mysql-server ... done
Creating redis-server ... done
WARNING: The "app-server" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.
Creating docker-demo-redis_app-server_1 ... done
Creating docker-demo-redis_app-server_2 ... done
```

app-serverコンテナで任意のコマンドを実行するには`--index=num`で、どのコンテナで実行するかnumで指定します。

```text
> docker-compose exec --index=1 app-server bash --login
```

## MySQL serverへの接続数を確認する

この記事のJavaアプリケーションは1インスタンスから同時に2つのコネクションを張るようになっています。
下記のshow processlistの結果から、Id #8,#9が172.22.0.4から、Id #10,#11が172.22.0.5から接続されていることがわかります。

```text
> show processlist;
+----+-----------------+------------------+-----------+---------+------+------------------------+------------------+
| Id | User            | Host             | db        | Command | Time | State                  | Info             |
+----+-----------------+------------------+-----------+---------+------+------------------------+------------------+
|  4 | event_scheduler | localhost        | NULL      | Daemon  |  843 | Waiting on empty queue | NULL             |
|  8 | test_user       | 172.22.0.4:48354 | sample_db | Sleep   |  674 |                        | NULL             |
|  9 | test_user       | 172.22.0.4:48356 | sample_db | Sleep   |  676 |                        | NULL             |
| 10 | test_user       | 172.22.0.5:40842 | sample_db | Sleep   |  263 |                        | NULL             |
| 11 | test_user       | 172.22.0.5:40844 | sample_db | Sleep   |  265 |                        | NULL             |
| 13 | root            | localhost        | NULL      | Query   |    0 | starting               | show processlist |
+----+-----------------+------------------+-----------+---------+------+------------------------+------------------+
6 rows in set (0.00 sec)
```

## Redis serverへの接続数を確認する

client id #3が172.22.0.4から、id #4が172.22.0.5から接続されていることがわかります。

```text
127.0.0.1:6379> client list
id=3 addr=172.22.0.4:37174 fd=9 name= age=1621 idle=1621 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=4 addr=172.22.0.5:44918 fd=10 name= age=1051 idle=1051 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=5 addr=127.0.0.1:41110 fd=11 name= age=79 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
```

## docker-compose scale

`docker-compose scale`はdeprecatedです。代わりにupコマンドで--scaleフラグを使用します。

> This command is deprecated. Use the up command with the --scale flag instead. Beware that using up with --scale flag has some subtle differences with the scale command as it incorporates the behaviour of up command.

## 実行するコンテナを増減させる

最初にapp-severを1つだけ実行します。

```text
> docker-compose up -d
Creating network "docker-demo-redis_redis-network" with driver "bridge"
Creating mysql-server ... done
Creating redis-server ... done
Creating docker-demo-redis_app-server_1 ... done
```

指定した数までコンテナをスケールアウトする

```text
> docker-compose up -d --scale app-server=2
redis-server is up-to-date
mysql-server is up-to-date
WARNING: The "app-server" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.
Starting docker-demo-redis_app-server_1 ... done
Creating docker-demo-redis_app-server_2 ... done
```

指定した数までコンテナを減らす

```text
> docker-compose up -d --scale app-server=1
redis-server is up-to-date
mysql-server is up-to-date
Stopping and removing docker-demo-redis_app-server_2 ... done
Starting docker-demo-redis_app-server_1              ... done
```