DockerオフィシャルのMySQL（MariaDB）コンテナの挙動をDockerfile周辺から読み解く

2018/3/12 環境変数 MYSQL_ROOT_HOST に関して追記しました。

# 動機

DockerオフィシャルのMySQL（MariaDB）のコンテナには環境変数や初期化スクリプトの投入機能があります。
便利に使える一方で、**「ただし、初回起動時のみ」**という制約が忘れられがちです。

この初回起動か否かというのは、コンテナ内の ```mysql``` ディレクトリの有無で判断されています（後述）。

MySQL（MariaDB）のコンテナでは、データベースという特性上、Dockerのボリューム機能を合わせて使うことが多いです。
そのため、docker-compose.yml で環境変数をいじったり、初期化スクリプトをちょっと修正して再実行しても**初回起動ではない**と判定されて、全くデータベースに反映されないということが普通に起こります。

知らないとハマってどうしようもなくなりますので、DockerオフィシャルのMySQL（MariaDB）の挙動についてここにまとめました。
情報はすべて2018年2月14日現在です。

MySQLのオフィシャルリポジトリは
https://hub.docker.com/_/mysql/

MariaDBのオフィシャルリポジトリは
https://hub.docker.com/r/mariadb/

となります。

それぞれ、オフィシャルリポジトリに起動オプションなど詳しい使い方が掲載されています。
```docker run``` でごにょごにょ遊ぶ方法なども紹介されていますので、一読をおすすめします。

オフィシャルリポジトリのDockerfileと、そこから呼び出される ```docker-entrypoint.sh``` がここでの分析の対象です。

# 初回起動かどうかの判別

シェルスクリプトを読める人は、短いですから ```docker-entrypoint.sh``` に目を通すのが一番簡単で確実です。

```:docker-entrypoint.sh
...
if [ ! -d "$DATADIR/mysql" ]; then
...
```

という一文で処理が分岐しています。
データボリュームに ```mysql``` というディレクトリがあるかどうか、それだけで初回起動かどうかを判定しています。

何かのはずみでDockerコンテナを起動する前にボリューム上に ```/var/lib/mysql/mysql``` などというディレクトリを作っていたりすると、予期せぬ挙動になる可能性があります。

# カスタム設定ファイルを使う

DockerオフィシャルのMySQL（MariaDB）サーバーは起動時に ```/etc/mysql/conf.d``` ディレクトリにある、拡張子 .cnf のファイルを設定ファイルとして読み込みます。
なので、例えばホスト側に ```/my/custom/config-file.cnf``` というファイルを用意しておき、docker-compose.ymlで

```:docker-compose.yml
...
services:

  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: my-secret-pw
    volumes:
      - /my/custom:/etc/mysql/conf.d
 ...
```

というようにボリュームを設定すると、起動時にその設定ファイルを読み込んでくれます。
ただし、Dockerfileをよく読むと

```
sed -ri 's/^user\s/#&/' /etc/mysql/my.cnf /etc/mysql/conf.d/*
```

という1行が入っていますので、userオプションだけは無効化されてしまいます。要注意です。

この機能については、**初回起動だけでなく、2回目以降の起動でも有効**です。

# 環境変数であれこれ

DockerオフィシャルのMySQL（MariaDB）コンテナは環境変数でいくつかの追加設定ができます。
ただし、いずれも**初回起動時でないと効果はない**ので注意してください。

## MYSQL_ROOT_PASSWORD

MySQL（MariaDB）のrootユーザーのパスワードを設定します。
初回起動時、

- MYSQL_ROOT_PASSWORD
- MYSQL_ALLOW_EMPTY_PASSWORD
- MYSQL_RANDOM_ROOT_PASSWORD

のいずれも設定されていないと、コンテナの起動は失敗します。
また、この環境変数は初回起動時のみ有効なので、一度起動したコンテナのrootのパスワードの変更には使えません。

一度でも起動してしまったら、その後はMySQL(MariaDB)クライアントでサーバーに接続し、パスワードを変更することになります。

## MYSQL_INITDB_SKIP_TZINFO

オフィシャルリポジトリの説明文では触れられていないのですが、これに適当な文字列を設定すると、初回起動時に実行されるタイムゾーンテーブルのロードがスキップされます。
ただしややこしいことに

```:docker-compose.yml
    environment:
      MYSQL_INITDB_SKIP_TZINFO: yes
```

とやっても

```:docker-compose.yml
    environment:
      MYSQL_INITDB_SKIP_TZINFO: no
```

とやっても、どちらでもスキップされるので要注意です。触らないほうがよさそうですね。

## MYSQL_RANDOM_ROOT_PASSWORD

この環境変数に適当な文字列を設定すると、初回起動時に32文字のランダムな文字列のrootパスワードが生成・設定され、設定されたパスワードが stdout に出力されます。
docker-composeの出力を取得できない環境の場合は使わないほうが良さそうです。

なおたとえば Debian/GNU Linux jessie の場合ですと、

```
$ pwgen -1 32
```

で同じロジックで生成されたパスワードが得られますので、これで出力された値をMYSQL_ROOT_PASSWORDに設定することを考えたほうがよいかもしれません。

## MYSQL_ROOT_HOST

この環境変数はオプションです。設定されていると、そのホストからrootユーザーとしてMySQLサーバーにアクセスすることができるようになります。
内部的には

```
CREATE USER 'root'@'a_host' IDENTIFIED BY 'a_root_password' ;
GRANT ALL ON *.* TO 'root'@'a_host' WITH GRANT OPTION ;
```

というSQLが発行されます。
この値が設定されていても、localhost から root としてMySQLサーバーにログインすることは可能です。

（2018/3/12 追記）

この環境変数が**設定されていないと**、内部的に

```
CREATE USER 'root'@'%' IDENTIFIED BY 'a_root_password' ;
GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
```

というSQLが発行され、**ネットワーク内のどこからでもrootユーザーとしてmysql serverにログインできる**ようになります。

これを避けたい場合は MYSQL_ROOT_HOST に localhost を指定する必要があります。

## MYSQL_ALLOW_EMPTY_PASSWORD

オプショナルです。この値が設定されていると、rootパスワードなしの設定になります。
ドキュメントには yes に設定すると〜 というようなことが書かれていますが、起動スクリプトにはそのようなチェック項目は入っていません。何か値が設定されていればrootパスワードなしになります。

```:docker-compose.yml
    environment:
        MYSQL_ALLOW_EMPTY_PASSWORD: no
```

でもrootパスワードなしです。
色々な意味で、**本当に自分が何をやっているのかわかっている**のでなければ、設定しないほうがいい環境変数です。

## MYSQL_DATABASE

この環境変数はオプショナルです。
初回起動時に、この環境変数に設定された名前のデータベースが作成されます。
内部的には

```
CREATE DATABASE IF NOT EXISTS `a_database` ;
```

のようなSQLが発行されます。

もしも、後述の MYSQL_USER、MYSQL_PASSWORDが設定されていると、そのユーザーにこのデータベースに対するGRANT ALLが発行され、データベースの操作に関する全権限が与えられます。
アプリケーション側でデータベースを作成する段取りになっていたりして、起動時に余計なデータベースを作られると困る場合には、この値は設定してはいけません。

## MYSQL_USER, MYSQL_PASSWORD

これらの値はオプショナルです。使う時は両方設定しないと効果はありません。片方だけの指定だと単に無視されます。エラーなども出ません。
内部的には

```
CREATE USER 'a_user`@'%' IDENTIFIED BY 'a_password' ;
```

のようなSQLが発行されます。

MYSQL_DATABASEが設定されていると、このユーザーに、そのデータベースに対する GRANT ALL が発行されます。

```
GRANT ALL ON `a_database`.* TO 'a_user'@'%' ; 
```

のようなSQLが発行されます。

## MYSQL_ONETIME_PASSWORD

この環境変数はMySQLの 5.6以上で有効です（2018年2月14日現在、MariaDBでは使えません）。
rootユーザーがパスワード期限切れ状態にセットされ、最初のログインでパスワード変更が強制されます。

# 初期化スクリプトの実行

MySQL、MariaDB共通です。
コンテナ内の ```/docker-entrypoint-initdb.d/``` ディレクトリに、拡張子が.sh、.sql、.sql.gzのファイルが入っていると、初回起動時にこれらのファイルが処理されます。
初回起動時のみですので、コンテナにファイルが残っていたとしても、2回目以降の起動で実行されてしまうことはありません。

逆に、初期化スクリプトに書き間違いなどがあった場合に、コンテナを立ち上げ直しても、変更は適用されないということになります（もちろんボリュームを使っていることが前提です）。要注意です。

コンテナ起動時に実行される ```docker-entrypoint.sh``` を見てみると

```:docker-entrypoint.sh
...
for f in /docker-entrypoint-initdb.d/*; do
    case "$f" in
        *.sh)     echo "$0: running $f"; . "$f" ;;
        *.sql)    echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;;
        *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;;
        *)        echo "$0: ignoring $f" ;;
    esac
    echo
done
...
```

のような処理になっています。
```/docker-entrypoint-initdb.d/``` ディレクトリ内のファイルをアルファベット順に読み出して、拡張子.shならそのまま実行、.sqlならMySQLに投入、.sql.gzならgunzipしてMySQLに投入、という処理になっています。

# MySQLとMariaDBの互換性

MariaDBのコンテナは、基本的に、MySQLのコンテナとそっくりそのまま入れ替えて使えるようになっています。
そのため、MariaDBのコンテナであっても、例えば環境変数名はMYSQL_で始まるようになっています。
これをMARIADB_などと書き換えても動作はしません。

ただし、細かいところを見ていくと、MariaDBにはMYSQL_ONETIME_PASSWORDに相当する機能がなかったり、Percona XtraBackupがデフォルトでインストールされていたりといった違いがあります。