MySQLのDockerイメージをイチから作成する

わけあって、公式のものとは別にMySQLのDockerイメージを作成しました。
この記事はその備忘録です。

公式のイメージはデータ保存の部分がVOLUMEマウントされているので、イメージの中にデータを含められないからです。

公式イメージではデータを保存できない

Docker環境でMySQLを利用したい場合はMySQLの公式イメージを使用するのが一番ラクで便利です。
ですが、公式イメージではデータを保存しておくことができません。 MySQLではデータを /var/lib/mysql というディレクトリ内に保存するのですが、このディレクトリがVOLUMEマウントの対象になっているためです。

「データをdockerイメージに含めるのはおかしい」という話はもちろんあるのですが、それでも含めたいときがあるじゃないですか。。。というお話です。

試行錯誤してみた

自分で色々とやってみて、たどり着いたのが以下でした。

Dockerfile

FROM ubuntu:18.04

ENV TZ Asia/Tokyo
ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        mysql-server \
        tzdata \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# timezone
WORKDIR /opt
RUN mysql_tzinfo_to_sql /usr/share/zoneinfo > timezone.sql

# mysql
RUN mkdir -p /var/run/mysqld \
    && chown mysql:mysql /var/run/mysqld \
    && usermod -d /var/lib/mysql mysql \
    && mv /etc/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/00_mysql.cnf \
    && mv /etc/mysql/conf.d/mysqldump.cnf /etc/mysql/conf.d/01_mysqldump.cnf \
    && mv /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/mysql.conf.d/00_mysqld.cnf \
    && mv /etc/mysql/mysql.conf.d/mysqld_safe_syslog.cnf /etc/mysql/mysql.conf.d/01_mysqld_safe_syslog.cnf \
    && sed -i -e "s~log_error~# log_error~g" /etc/mysql/mysql.conf.d/00_mysqld.cnf \
    && sed -i -e "s~bind_address~# bind_address~g" /etc/mysql/mysql.conf.d/00_mysqld.cnf

COPY original_mysql.cnf /etc/mysql/conf.d/02_original_mysql.cnf
COPY original_client.cnf /etc/mysql/conf.d/03_original_client.cnf
COPY original_mysqld.cnf /etc/mysql/mysql.conf.d/02_original_mysqld.cnf

COPY entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh

EXPOSE 3306

ENTRYPOINT ["./entrypoint.sh"]

entrypoint.sh

#!/bin/sh

set -e

find /var/lib/mysql -type f -exec touch {} \;

if [ ! -f timezone_applied ];then
  mysqld &
  sleep 3
  mysql -uroot -D mysql < timezone.sql
  mysqladmin shutdown -uroot
  touch timezone_applied
fi

mysqld

ひとつずつ解説していきます。

Dockerfile

FROM ubuntu:18.04

コンテナと言えばAlpine Linuxですが、Alpineのパッケージ管理ツール(apk)では、デフォルトではMySQLをインストールできません。
MySQL互換のMariaDBならインストールすることができますが、MariaDBは認証まわりがMySQLとだいぶ違うため、今回は見送りました。
Alpine以外なら何でも大丈夫だと思います。Ubuntuは私の趣味です。

ENV TZ Asia/Tokyo

TZ というタイムゾーンを指定することで、MySQLタイムゾーンの指定をすることができます。1

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        mysql-server \
        tzdata \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

MySQLとtzdataをインストールしています。
tzdataはタイムゾーンの情報が入ったパッケージです。
MySQLではデフォルトではタイムゾーンの情報が入っていないため、このtzdataをベースにタイゾーン情報を入れていきます。

また、tzdataをインストールする際に対話シェルが開始されます。
Dockerイメージをビルドする際に対話シェルが開始されると処理が止まってしまうので、 DEBIAN_FRONTEND を指定して対話シェルが開始されないようにしています。

apt使用後はaptに関するデータは不要になるので、最後に使用されていないパッケージや更新されたリポジトリリストを削除しています。

WORKDIR /opt

ここからはファイルを生成したりしていくので、ルートディレクトリから移動をしています。
どこでもいいので、とりあえず /opt に設定しています。

RUN mysql_tzinfo_to_sql /usr/share/zoneinfo > timezone.sql

先ほどインストールしたtzdataの情報をSQLに変換しています。
このSQLはentrypointで使用します。

RUN mkdir -p /var/run/mysqld \
    && chown mysql:mysql /var/run/mysqld \

MySQLのデーモンがpidファイルを作成するディレクトリを作成しています。
また、Dockerイメージをビルドする際は基本的にrootユーザなので、ディレクトリのオーナーもmysqlに変更しています。

    && usermod -d /var/lib/mysql mysql \

mysqlユーザのホームディレクトリが設定されていない場合、 No directory, logging in with HOME=/ というエラーが発生するので、ここでmysqlユーザのホームディレクトリを設定しています。

    && mv /etc/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/00_mysql.cnf \
    && mv /etc/mysql/conf.d/mysqldump.cnf /etc/mysql/conf.d/01_mysqldump.cnf \
    && mv /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/mysql.conf.d/00_mysqld.cnf \
    && mv /etc/mysql/mysql.conf.d/mysqld_safe_syslog.cnf /etc/mysql/mysql.conf.d/01_mysqld_safe_syslog.cnf \

Debian系のLinuxでは、設定をひとつのmy.cnfに書かずに、それぞれの設定をファイルに分割して書きます。 その際にファイル名の順番で読み込まれていきます。
読み込まれる設定の順番を管理するために、デフォルトの設定ファイルに連番のプレフィックスを割り振っています。

ちなみに、 /etc/mysql/mysql.conf.d/ がデーモン寄りの設定が入っており、 /etc/mysql/conf.d/ にクライアント寄りの設定が入っているようです。

    && sed -i -e "s~log_error~# log_error~g" /etc/mysql/mysql.conf.d/00_mysqld.cnf \
    && sed -i -e "s~bind_address~# bind_address~g" /etc/mysql/mysql.conf.d/00_mysqld.cnf

デフォルトの設定のいくつかを無効化(コメントアウト)しています。
Dockerではログファイルにログを吐かれても仕方がないので、 log_error を無効にしています。
また、Dockerコンテナを使う以上、外部ネットワークからの接続を期待するので、 bind_address を無効にしています。

COPY original_mysql.cnf /etc/mysql/conf.d/02_original_mysql.cnf
COPY original_client.cnf /etc/mysql/conf.d/03_original_client.cnf
COPY original_mysqld.cnf /etc/mysql/mysql.conf.d/02_original_mysqld.cnf

自分の作成した設定ファイルをコピーしています。

COPY entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh

自分で作成したエントリーポイントを使用したいため、コピーしてきています。
また、実行可能にするために実行権限を付与しています。

EXPOSE 3306
ENTRYPOINT ["./entrypoint.sh"]

MySQLなので3306番ポートを開き、エントリーポイントを設定しています。

entrypoint.sh

#!/bin/sh
set -e

set -e を指定すると、エラー発生時にスクリプトを止めてくれます。

find /var/lib/mysql -type f -exec touch {} \;

Dockerのストレージエンジン(overlay2)とMySQLの相性が悪いらしく、そのままではCan’t open and lock privilege tables: Table storage engine for ‘user’ doesn’t have this optionというエラーが発生します。
このコメントを参考にさせていただきました。

if [ ! -f timezone_applied ];then

このあとの処理で、タイムゾーンの設定が完了した際に timezone_applied というファイルを作成しています。
つまり、タイムゾーン設定が完了していない場合にタイムゾーンの設定を行っています。

  mysqld &
  sleep 3

mysqldを起動し、起動が完了するまで待機しています。
3秒なのは手抜きです。本当は起動完了しているかどうかを何かしらの方法でチェックする必要があります。

  mysql -uroot -D mysql < timezone.sql

MySQLタイムゾーンのデータをインポートしています。
timezone.sql はDockerfile内で作成しました。

  mysqladmin shutdown -uroot
  touch timezone_applied
fi

mysqldを終了し、 timezone_applied ファイルを作成しています。
これで2回目以降はタイムゾーンの設定がスキップされます。

mysqld

MySQLを実行します。実質のentrypointです。

まとめ

自分でMySQLのイメージを作成してみました。
公式のMySQLのDockerfileがめちゃくちゃ勉強になります。
最初は全く読めないので、こちらの記事がとても参考になりました。