DockerでMySQLを構築する方法|起動・接続・永続化を実測で解説

環境&ワークフロー

DockerでMySQLを動かすとは、MySQLサーバーをホストに直接インストールせず、公式イメージから作ったコンテナとして起動することです。apt installも設定ファイルの編集も不要で、コマンド1行でMySQL 8.0が立ち上がり、不要になればコンテナごと消せます。この記事では、Ubuntu上のDockerで実際にコンテナを起動し、すべての実行結果を実測した出力で確認していきます。

docker run -d --name mysql-db -p 13306:3306 \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_DATABASE=appdb \
  mysql:8.0

結論から言うとこの1行で足ります。ただし初心者がつまずくのは「起動コマンド」ではなく、起動した“後”です。コンテナはUpなのに接続できない、パスワードを変えたのに反映されない、コンテナを作り直したらデータが消えた——この記事はそこを実測で潰していきます。

  1. DockerでMySQLを起動する最短手順
  2. 「Up」と「接続できる」は別物|起動直後につながらない理由
    1. 「ready for connections」は2回出る|portの値が決め手
    2. 初期化中に出る「Access denied」は、パスワードの間違いではない
    3. healthcheckは「-h 127.0.0.1」を付けないと早すぎる判定になる
  3. MySQLコンテナに接続する3つの方法
    1. (1) コンテナの中から接続する
    2. (2) 別のコンテナから接続する
    3. (3) ホストOSから接続する
  4. データを永続化する|-vを付けないとdocker rmで消える
    1. ボリューム無し:作り直すとテーブルごと消える
    2. 名前付きボリューム有り:削除しても残る
    3. 名前付きボリュームとバインドマウントの使い分け
  5. 環境変数を変えたのに反映されない|初期化は最初の1回だけ
  6. 初期データを自動で流し込む|docker-entrypoint-initdb.d
    1. MYSQL_USERで作られる一般ユーザーの権限範囲
  7. 文字コードとタイムゾーン|日本語環境で最初に直す2点
  8. docker composeでまとめる|そのまま使える設定
    1. depends_onだけでは起動順は守れない
    2. command:はシェルを通らない
  9. よくあるエラーと対処
    1. コンテナがすぐ終了する
    2. port is already allocated
  10. バックアップと復元|mysqldumpをコンテナ越しに使う
  11. 後片付け|検証が終わったら消す
  12. よくある質問
    1. Q. mysql:8.0とmysql:latestはどちらを使うべきですか?
    2. Q. MySQLとMariaDBのどちらのイメージを使えばよいですか?
    3. Q. Apple Silicon(M1/M2)のMacで動きますか?
    4. Q. コンテナを再起動したらデータは消えますか?
    5. Q. パスワードを変えたのに反映されません
    6. Q. 本番環境でDBをDockerで動かしてもよいですか?
    7. Q. -p 3306:3306で公開しても大丈夫ですか?
    8. Q. 起動を待つ処理をシェルスクリプトで書くには?
  13. まとめ
  14. 関連記事
  15. 関連記事

DockerでMySQLを起動する最短手順

まずイメージを取得します。mysql:8.0のようにバージョンを明示するのが基本です。mysql:latestだと、ある日docker pullした瞬間にメジャーバージョンが上がって動かなくなることがあります。

docker pull mysql:8.0

次にコンテナを起動します。実務で最低限指定したいオプションを全部入れた形が次のコマンドです。

docker run -d --name mysql-db --network bashdo-net -p 13306:3306 \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_DATABASE=appdb \
  -e MYSQL_USER=appuser -e MYSQL_PASSWORD=apppass \
  mysql:8.0
オプション意味省略すると
-dバックグラウンドで起動ターミナルがログで占有される
--nameコンテナ名を付けるランダム名になりexecが面倒
-p 13306:3306ホストの13306をコンテナの3306へ転送ホストから接続できない
-v mysql-data:/var/lib/mysqlデータをボリュームに保存コンテナ削除でデータ全消失
-e MYSQL_ROOT_PASSWORDrootのパスワードコンテナが起動せず終了
-e MYSQL_DATABASE初回に作るDB名DBを手動で作る必要がある
-e MYSQL_USER / MYSQL_PASSWORD一般ユーザーを作成rootで作業することになる

起動できたかdocker psで確認します。ここでいきなり最初の落とし穴が見えます。

docker ps --filter name=mysql-db --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES      STATUS                  PORTS
mysql-db   Up Less than a second   33060/tcp, 0.0.0.0:13306->3306/tcp, [::]:13306->3306/tcp

Up Less than a secondと表示されました。しかしこの時点でMySQLにはまだ接続できません。理由は次の章で実測します。

「Up」と「接続できる」は別物|起動直後につながらない理由

DockerでMySQLを扱うときに最も多い質問が「コンテナは起動しているのにCan't connectと出る」です。これはバグでも設定ミスでもなく、コンテナのプロセスが起動した時刻と、MySQLがTCPで待ち受けを始める時刻がずれているだけです。

起動から2秒後に、別コンテナから接続を試みた実際の結果がこちらです。

docker run -d --name mysql-b2 --network bashdo-net -e MYSQL_ROOT_PASSWORD=demopass mysql:8.0
sleep 2
docker run --rm --network bashdo-net mysql:8.0 mysql -h mysql-b2 -uroot -pdemopass -e "SELECT 1"
ERROR 2003 (HY000): Can't connect to MySQL server on 'mysql-b2:3306' (111)

末尾の(111)はLinuxのエラー番号ECONNREFUSED(接続拒否)です。ホスト名は解決できていて、そこまでパケットは届いているが、3306番で待ち受けているプロセスがまだいない状態を意味します。名前解決の失敗ではないので、ネットワーク設定を疑う必要はありません。

では何秒待てばよいのか。1秒ごとに接続を試して計測しました。

t=1s  State=running  ping失敗
t=2s  State=running  ping失敗
t=3s  State=running  ping失敗
t=7s  State=running  ping失敗
t=8s  State=running  mysqladmin ping => 成功

この環境では約8〜13秒かかりました。Stateは最初からrunningのままです。つまりdocker psUp表示は「コンテナのプロセスが動いている」ことしか保証しません。

「ready for connections」は2回出る|portの値が決め手

起動が完了したかを正確に知るには、ログを見るのが確実です。ただしここに罠があります。初期化のログを抜き出してみます。

docker logs mysql-db 2>&1 | grep -E "Entrypoint|ready for connections"
[Note] [Entrypoint]: Initializing database files
[Note] [Entrypoint]: Database files initialized
[Note] [Entrypoint]: Starting temporary server
[Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.46'  socket: '/var/run/mysqld/mysqld.sock'  port: 0
[Note] [Entrypoint]: Temporary server started.
[Note] [Entrypoint]: Stopping temporary server
[Note] [Entrypoint]: Temporary server stopped
[Note] [Entrypoint]: MySQL init process done. Ready for start up.
[Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.46'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306

ready for connections2回出ています。1回目はport: 0、2回目がport: 3306です。

1回目は初期化専用の一時サーバー(temporary server)です。MySQLの公式イメージは、データディレクトリが空のとき、まずデータベースの土台を作り、その後rootのパスワード設定や初期SQLを流し込むために一時的なサーバーを起動します。この一時サーバーはport: 0、つまりTCPで待ち受けていません(UNIXソケット専用)。設定が終わると一度停止し、あらためて本番サーバーがport: 3306で起動します。

したがって、起動完了を待つ条件はready for connectionsではなく、port: 3306付きの行です。

until docker logs mysql-db 2>&1 | grep -q "ready for connections.*port: 3306"; do sleep 1; done
echo "MySQLの準備が完了しました"

初期化中に出る「Access denied」は、パスワードの間違いではない

この一時サーバーの存在は、もう1つの混乱を生みます。起動直後に1秒ごとにSELECT 1を投げた結果を見てください。

t=5s  ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)
t=6s  ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
t=7s  ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
t=8s  ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)
t=9s  ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)
t=10s 1

エラーの種類が2002 → 1045 → 2002 → 成功と行ったり来たりしています。t=6〜7秒のAccess deniedは、一時サーバーが動いていて、まだrootのパスワードが設定される前に接続してしまったために出ています。

ここでMYSQL_ROOT_PASSWORDの値を疑って環境変数を書き換えるのが、典型的な遠回りです。数秒待てば同じパスワードで接続できます。起動直後のAccess deniedは、まず待ってから判断してください。

healthcheckは「-h 127.0.0.1」を付けないと早すぎる判定になる

起動待ちにはDockerのhealthcheckを使うのが定石で、mysqladmin pingを使う例がよく紹介されます。ただし書き方によっては、一時サーバーに反応して「healthy」と誤判定します

初期化中に、ソケット経由のpingとTCP経由のpingの終了ステータスを1秒ごとに比較しました。

経過   socket ping  TCP ping     ログ上の状態
t=6s   exit=1       exit=1       Initializing database files
t=7s   exit=0       exit=1       Temporary server started
t=8s   exit=1       exit=1       Temporary server started
t=9s   exit=1       exit=1       Temporary server started
t=10s  exit=0       exit=0       port: 3306

t=7秒の時点で、ソケット経由のpingだけがexit=0(成功)を返しています。本番サーバーが立ち上がるのはt=10秒なので、3秒早く「準備完了」と判定してしまうわけです。healthcheckがこれを拾うと、depends_onで待たせたアプリが早すぎるタイミングで起動し、冒頭のERROR 2003を踏みます。

一時サーバーはport: 0でTCPを開いていないため、-h 127.0.0.1を付けてTCPで検査すれば正しく判定できます

# NG: ソケット経由なので一時サーバーに反応する
mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD"

# OK: TCPで検査する
mysqladmin ping -h 127.0.0.1

なおmysqladmin pingには、もう1つ知っておくべき挙動があります。パスワードが間違っていても終了ステータスは0になります

docker exec mysql-db mysqladmin ping -h 127.0.0.1 -uroot -pWRONGPASS; echo "exit=$?"
mysqladmin: connect to server at '127.0.0.1' failed
error: 'Access denied for user 'root'@'127.0.0.1' (using password: YES)'
exit=0

エラーメッセージは出ているのにexit=0です。mysqladmin pingが確認しているのは「サーバーが応答するか」だけで、「ログインできるか」ではありません。healthcheckにパスワードを書いても認証の検証にはならないので、上記のように省略したほうが、余計な情報を設定ファイルに残さずに済みます。

MySQLコンテナに接続する3つの方法

接続経路は3つあり、それぞれ指定するホスト名が変わります。ここを混同するとERROR 2003ERROR 2002が出ます。

どこからホスト名の指定-p公開
コンテナの中不要(ソケット接続)不要
同じDockerネットワークの別コンテナコンテナ名/サービス名不要
ホストOS(手元のPC)127.0.0.1 + 公開ポート必要

(1) コンテナの中から接続する

いちばん手軽なのがdocker execです。対話的に触るなら-itを付けます。

docker exec -it mysql-db mysql -uroot -p

パスワードは対話的に聞かせるのが安全です。-prootpassのようにコマンドラインに直接書くと、そのホストのpsコマンドで他のユーザーからパスワードが見えてしまいます。1行で結果だけ欲しい場合は-eを使います。

docker exec -i mysql-db mysql -uroot -prootpass -e "SELECT VERSION() AS version; SHOW DATABASES;"
version
8.0.46
Database
appdb
information_schema
mysql
performance_schema
sys

-eで指定したMYSQL_DATABASE=appdbが作られていることが確認できます。

(2) 別のコンテナから接続する

アプリのコンテナからDBに接続する、実務でいちばん多いパターンです。同じDockerネットワークに所属していれば、コンテナ名がそのままホスト名として名前解決されます

docker run --rm --network bashdo-net mysql:8.0 \
  mysql -h mysql-db -uappuser -papppass -e "SELECT '別コンテナから接続OK' AS result;"
result
別コンテナから接続OK

この経路では-pによるポート公開は不要です。コンテナ同士はDockerネットワークの内側で直接通信できます。逆に言えば、アプリからしか使わないDBをホストに公開する必要はありません。

なお、ここで-h localhostと書くのは典型的な誤りです。コンテナにとってのlocalhost自分自身を指すため、MySQLではなくアプリコンテナ自身に接続しにいって失敗します。

(3) ホストOSから接続する

手元のGUIツールやmysqlコマンドから触りたい場合は、-pでポートを公開します。

mysql -h 127.0.0.1 -P 13306 -uappuser -p
server        mysql_port
713cc8c6927b  3306

興味深いのは@@port3306と返ってくる点です。-p 13306:3306は「ホストの13306番宛の通信をコンテナの3306番へ転送する」というホスト側だけの設定で、MySQL自身は自分が13306番で公開されていることを知りません。@@hostnameがコンテナIDになっているのも同じ理由です。

ホスト名にlocalhostではなく127.0.0.1を使うのがポイントです。MySQLのクライアントは-h localhostを指定されるとTCPではなくUNIXソケットで接続しようとするため、ホスト側にMySQLが入っていないと次のエラーになります。

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

データを永続化する|-vを付けないとdocker rmで消える

コンテナは使い捨てが前提です。コンテナ内に書いたデータは、docker rmした瞬間に消えます。DBでこれをやると事故になるので、実際に消えるところを確認しておきます。

ボリューム無し:作り直すとテーブルごと消える

docker run -d --name mysql-novol -e MYSQL_ROOT_PASSWORD=demopass -e MYSQL_DATABASE=appdb mysql:8.0
# 起動を待ってからデータを投入
docker exec mysql-novol mysql -uroot -pdemopass \
  -e "CREATE TABLE appdb.memo(id INT, body VARCHAR(30)); INSERT INTO appdb.memo VALUES (1,'volume-less');"
docker exec mysql-novol mysql -uroot -pdemopass -e "SELECT * FROM appdb.memo;"
id      body
1       volume-less

ここでコンテナを削除し、まったく同じコマンドで作り直します。

docker rm -f mysql-novol
docker run -d --name mysql-novol -e MYSQL_ROOT_PASSWORD=demopass -e MYSQL_DATABASE=appdb mysql:8.0
docker exec mysql-novol mysql -uroot -pdemopass -e "SELECT * FROM appdb.memo;"
ERROR 1146 (42S02) at line 1: Table 'appdb.memo' doesn't exist

テーブルごと消えました。appdbMYSQL_DATABASEで作り直されているのでデータベースは存在しますが、中身は空です。この「DBはあるのにテーブルが無い」状態は原因が分かりにくく、混乱しやすいポイントです。

名前付きボリューム有り:削除しても残る

-v ボリューム名:/var/lib/mysqlを付けて同じことをします。

docker run -d --name mysql-vol -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=demopass -e MYSQL_DATABASE=appdb mysql:8.0
docker exec mysql-vol mysql -uroot -pdemopass \
  -e "CREATE TABLE appdb.memo(id INT, body VARCHAR(30)); INSERT INTO appdb.memo VALUES (1,'with-volume');"

docker rm -f mysql-vol
docker run -d --name mysql-vol -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=demopass -e MYSQL_DATABASE=appdb mysql:8.0
docker exec mysql-vol mysql -uroot -pdemopass -e "SELECT * FROM appdb.memo;"
id      body
1       with-volume

残りました。MySQLのデータ本体は/var/lib/mysqlにあり、ここをボリュームに逃がしておけばコンテナの寿命と切り離せます。

副次的な効果として、2回目以降の起動が明確に速くなります。初期化処理が丸ごとスキップされるためです。

状況接続できるまでの時間ログの「Initializing database files」
初回起動(データ空)12〜13秒出る
2回目以降(ボリューム有り)3秒出ない

名前付きボリュームとバインドマウントの使い分け

名前付きボリュームバインドマウント
書き方-v mysql-data:/var/lib/mysql-v ./data:/var/lib/mysql
実体の場所Dockerが管理する領域指定したホストのディレクトリ
権限問題起きにくいUID不一致で起動失敗しやすい
中身を直接見るしにくいしやすい
おすすめDBのデータ本体初期化SQLや設定ファイル

DBのデータ本体は名前付きボリュームが安全です。バインドマウントはホスト側のディレクトリ所有者とコンテナ内のmysqlユーザーのUIDが食い違うと、権限エラーで起動しません。一方、設定ファイルや初期化SQLのように「こちらから読ませたいファイル」はバインドマウントが向いています。

ボリュームの一覧と削除は次のコマンドです。docker rmではボリュームは消えないため、放置すると溜まっていきます。

docker volume ls
docker volume rm mysql-data

環境変数を変えたのに反映されない|初期化は最初の1回だけ

永続化の裏返しとして、もっとも質問が多い挙動がこれです。

demopassで初期化済みのボリュームを使い、MYSQL_ROOT_PASSWORD=newpassを指定して起動してみます。

docker run -d --name mysql-pwd -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=newpass mysql:8.0

# 新しく指定した newpass で接続
docker exec mysql-pwd mysql -uroot -pnewpass -e "SELECT 1"
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
# 古い demopass で接続
docker exec mysql-pwd mysql -uroot -pdemopass -e "SELECT 'demopass はまだ有効' AS result;"
result
demopass はまだ有効

新しいパスワードは完全に無視され、初期化時のパスワードが生き続けています。ログを見るとInitializing database filesの行が出ていません。

公式イメージの起動スクリプトは/var/lib/mysqlが空のときだけ初期化を実行します。ボリュームに既存データがあれば「初期化済み」と判断し、MYSQL_ROOT_PASSWORDMYSQL_DATABASEMYSQL_USERはすべて読み捨てられます。

これはdocker-entrypoint-initdb.dに置いた初期化SQLでも同じです。既存ボリュームのあるコンテナをdocker restartしても、新しく追加したSQLファイルは実行されません(ログにファイル名すら出ません)。

やりたいこと正しい方法
開発環境で作り直したいdocker volume rmでボリュームごと削除して再作成
データを残したままパスワードだけ変えたいSQLでALTER USERを実行する
スキーマを追加したいSQLを直接流す(マイグレーションツール等)

ボリュームを消して作り直すと、指定どおりのパスワードで初期化されます。

docker rm -f mysql-pwd && docker volume rm mysql-data
docker run -d --name mysql-pwd -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=newpass mysql:8.0
docker exec mysql-pwd mysql -uroot -pnewpass -e "SELECT 'newpass で接続できた' AS result;"
result
newpass で接続できた

データを残したままパスワードを変える場合はSQLで行います。

docker exec -it mysql-db mysql -uroot -p \
  -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '新しいパスワード'; FLUSH PRIVILEGES;"

初期データを自動で流し込む|docker-entrypoint-initdb.d

公式イメージには、初回起動時に指定ディレクトリのSQLを自動実行する仕組みがあります。/docker-entrypoint-initdb.d.sql.sh.sql.gzを置くだけです。テーブル定義やテストデータの投入に使えます。

mkdir -p initdb
cat > initdb/01_schema.sql <<'EOF'
CREATE TABLE IF NOT EXISTS appdb.users (
  id   INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL
);
INSERT INTO appdb.users (name) VALUES ('田中'), ('鈴木');
EOF

これをバインドマウントして起動します。読み取り専用にする:roを付けておくと安全です。

docker run -d --name mysql-init \
  -e MYSQL_ROOT_PASSWORD=demopass -e MYSQL_DATABASE=appdb \
  -e MYSQL_USER=appuser -e MYSQL_PASSWORD=apppass \
  -v "$(pwd)/initdb:/docker-entrypoint-initdb.d:ro" mysql:8.0

ログに実行されたファイル名が出ます。

[Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01_schema.sql

一般ユーザーで中身を確認します。

docker exec mysql-init mysql -uappuser -papppass appdb -e "SELECT * FROM users;"
id      name
1       田中
2       鈴木

ファイルは名前順に実行されるため、01_02_と連番を付けて依存関係を制御します。ここでも「実行されるのは初回だけ」の原則が効くので、SQLを修正したらボリュームを消して作り直してください。

MYSQL_USERで作られる一般ユーザーの権限範囲

MYSQL_USERで作られたユーザーには、MYSQL_DATABASEで指定したDBへの全権限だけが付きます。SHOW DATABASESを比べると差が分かります。

# appuser
Database
appdb
information_schema
performance_schema

# root
Database
appdb
information_schema
mysql
performance_schema
sys

appuserからはmysql(ユーザー情報を保持する管理用DB)とsysが見えません。アプリからの接続はrootではなくこの一般ユーザーを使うのが基本です。

文字コードとタイムゾーン|日本語環境で最初に直す2点

起動したままの状態を確認します。

docker exec mysql-db mysql -uroot -prootpass \
  -e "SHOW VARIABLES LIKE 'character_set_server'; SELECT @@system_time_zone, NOW();"
Variable_name          Value
character_set_server   utf8mb4

@@system_time_zone     NOW()
UTC                    2026-08-02 13:28:05

文字コードは対応不要です。MySQL 8.0の既定はutf8mb4(照合順序はutf8mb4_0900_ai_ci)で、絵文字を含む日本語がそのまま扱えます。5.7時代の記事にあるcharacter-set-server=utf8mb4の追記は、8.0では基本的に不要です。

問題はタイムゾーンです。NOW()がUTCを返すため、日本時間との9時間ずれがそのままレコードに入ります。修正方法は2つあります。

# 方法1: コンテナのOSごとJSTにする
docker run -d --name mysql-tz1 -e MYSQL_ROOT_PASSWORD=demopass -e TZ=Asia/Tokyo mysql:8.0
system_tz  now
JST        2026-08-02 22:28:34
# 方法2: MySQLの設定だけ変える(イメージの引数として渡す)
docker run -d --name mysql-tz2 -e MYSQL_ROOT_PASSWORD=demopass mysql:8.0 --default-time-zone=+09:00
system_tz  time_zone  now
UTC        +09:00     2026-08-02 22:28:48

どちらもNOW()は日本時間になります。違いは@@system_time_zoneで、方法1はコンテナのOSごと、方法2はMySQLだけがJSTになります。コンテナのログの時刻もJSTにしたいなら方法1を選んでください。

なおmysql:8.0のあとに書いた--default-time-zone=+09:00は、Dockerのオプションではなくmysqldに渡される引数です。この位置には--character-set-serverなど、MySQLの起動オプションを自由に追加できます。

docker composeでまとめる|そのまま使える設定

docker runのオプションが長くなってきたらdocker composeに移します。ここまでの内容をすべて反映した、動作確認済みの設定です。

まずパスワードを.envに分離します。.envは必ず.gitignoreに追加してください。

MYSQL_ROOT_PASSWORD=rootpass
MYSQL_DATABASE=appdb
MYSQL_USER=appuser
MYSQL_PASSWORD=apppass

compose.ymlはこうなります。

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    env_file: .env
    environment:
      TZ: Asia/Tokyo
    ports:
      - "13306:3306"
    volumes:
      - db-data:/var/lib/mysql
      - ./initdb:/docker-entrypoint-initdb.d:ro
    healthcheck:
      # -h 127.0.0.1 でTCPを検査する(ソケット検査だと初期化中の一時サーバーに反応する)
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 30s

  app:
    image: mysql:8.0
    depends_on:
      db:
        condition: service_healthy
    env_file: .env
    command:
      - sh
      - -c
      - >
        mysql -h db -u"$$MYSQL_USER" -p"$$MYSQL_PASSWORD" appdb
        -e "SELECT 'appからdbへ接続成功' AS result; SELECT * FROM users;"

volumes:
  db-data:
docker compose up
 Container composedemo-db-1   Healthy
 Container composedemo-app-1  Starting
 Container composedemo-app-1  Started
app-1  | result
app-1  | appからdbへ接続成功
app-1  | id      name
app-1  | 1       田中
app-1  | 2       鈴木

dbHealthyになってからappが起動し、1回もリトライせずに接続できています。初回起動は18秒、2回目以降(ボリューム有り)は7秒でした。

depends_onだけでは起動順は守れない

ここで重要なのがcondition: service_healthyです。

# これはNG:dbのコンテナが起動した瞬間にappが動き出す
depends_on:
  - db

# これがOK:healthcheckがhealthyを返すまで待つ
depends_on:
  db:
    condition: service_healthy

素のdepends_onが保証するのは「コンテナの起動順」だけで、中のMySQLが応答できるかは見ていません。この記事の冒頭で測ったとおりMySQLは起動に8〜13秒かかるため、素のdepends_onだとアプリがERROR 2003で落ちます。healthcheckとセットで初めて機能します。

command:はシェルを通らない

上の設定でcommand:sh -cで包んでいるのには理由があります。包まずに直接書くと、環境変数が展開されません。

ERROR 1045 (28000): Access denied for user '$MYSQL_USER'@'172.19.0.3' (using password: YES)

ユーザー名が$MYSQL_USERという文字列そのままで渡っています。command:はシェルを経由せずプロセスを直接起動するため、変数展開もパイプもリダイレクトも効きません。変数を使いたいときはsh -cで包むのが定石です。

またcompose.ymlの中では$$$と二重に書きます。Compose自身が$VARを先に展開してしまうため、$$でエスケープしてコンテナ側に渡す必要があります。

よくあるエラーと対処

エラー原因対処
ERROR 2003 ... (111)MySQLがまだ待ち受けていない/ポート未公開数秒待つ。ホストからなら-pを確認
ERROR 2002 ... through socket-h localhostでソケット接続になっている-h 127.0.0.1にする
ERROR 1045 Access denied初期化中/既存ボリュームの古いパスワード待つ。それでも駄目ならボリュームを確認
コンテナが即Exited (1)パスワード系の環境変数が未指定MYSQL_ROOT_PASSWORDを指定
port is already allocatedホスト側のポートが使用中公開ポートを変える
テーブルが消えた-v無しでコンテナを作り直したボリュームを付けて運用する

コンテナがすぐ終了する

パスワード系の環境変数を1つも指定しないと、コンテナは起動せずに終了します。docker psでは見えないので-aを付けて確認します。

docker run -d --name mysql-nopass mysql:8.0
docker ps -a --filter name=mysql-nopass --format 'table {{.Names}}\t{{.Status}}'
NAMES          STATUS
mysql-nopass   Exited (1) 2 seconds ago

原因はdocker logsに明記されています。コンテナが即終了したときは、まずdocker logsを見るのが鉄則です。

docker logs mysql-nopass
[ERROR] [Entrypoint]: Database is uninitialized and password option is not specified
    You need to specify one of the following as an environment variable:
    - MYSQL_ROOT_PASSWORD
    - MYSQL_ALLOW_EMPTY_PASSWORD
    - MYSQL_RANDOM_ROOT_PASSWORD

3つのうちどれか1つを指定すれば起動します。MYSQL_ALLOW_EMPTY_PASSWORD=yesはパスワード無しでrootに入れてしまうため、手元の使い捨て検証以外では使わないでください

port is already allocated

同じホストポートを2つのコンテナで使おうとしたときのエラーです。

docker: Error response from daemon: failed to set up container networking: driver failed programming
external connectivity on endpoint mysql-conflict: Bind for 0.0.0.0:13306 failed: port is already allocated

ホストにMySQLを直接インストールしている場合、3306番はすでに使われています。この記事で-p 13306:3306としているのは、その衝突を避けるためです。何が掴んでいるかはssで確認できます。

ss -ltnp | grep 13306
LISTEN 0      4096      0.0.0.0:13306      0.0.0.0:*
LISTEN 0      4096         [::]:13306         [::]:*

詳しい切り分けはLinuxのポート確認方法とポート競合の解消手順にまとめています。

バックアップと復元|mysqldumpをコンテナ越しに使う

ボリュームでデータは残りますが、誤ってDROPしたときには救えません。論理バックアップも取っておきます。docker execの標準出力をホストのファイルへリダイレクトするのが基本形です。

docker exec mysql-db mysqldump -uroot -prootpass --single-transaction --databases appdb > appdb.sql

--single-transactionはInnoDBのテーブルをロックせずに整合性のあるダンプを取るオプションで、稼働中のDBを止めずにバックアップできます。復元は逆向きに標準入力から流し込みます。

docker exec -i mysql-db mysql -uroot -prootpass < appdb.sql

実際にデータベースを削除してから復元した結果です。

# DROP後
ERROR 1049 (42000) at line 1: Unknown database 'appdb'

# 復元後
id      v
1       元データ

復元時は-i(標準入力を渡す)が必要です。-itと書くと端末が割り当てられてリダイレクトが壊れるので、スクリプトの中では-iだけにします。オプションの詳細はmysqldump – MySQL/MariaDBの論理バックアップを作成するダンプツールを参照してください。

後片付け|検証が終わったら消す

# コンテナを停止・削除
docker rm -f mysql-db

# ボリュームも消す(データが完全に消えるので注意)
docker volume rm mysql-data

# composeの場合(-v を付けるとボリュームも消える)
docker compose down -v

docker compose down-vを付けない限りボリュームを残します。逆に言えば、-vを付けたdownはDBを完全に消すコマンドです。本番系では絶対に打たないでください。

よくある質問

Q. mysql:8.0とmysql:latestはどちらを使うべきですか?

バージョンを明示したmysql:8.0を推奨します。latestdocker pullのタイミングでメジャーバージョンが変わる可能性があり、ある日突然アプリが動かなくなります。データファイルの形式も変わるため、既存ボリュームのまま上げると起動しないことがあります。

Q. MySQLとMariaDBのどちらのイメージを使えばよいですか?

接続先の本番環境に合わせてください。この記事で扱った環境変数(MYSQL_ROOT_PASSWORDなど)やdocker-entrypoint-initdb.dの仕組みはmariadbイメージでもほぼ同じように使えます。

Q. Apple Silicon(M1/M2)のMacで動きますか?

MySQL 8.0の公式イメージはarm64に対応しているため、そのまま動きます。古い5.7を使いたい場合はarm64版が無いことがあり、その際は--platform linux/amd64を指定してエミュレーションで動かすか、mariadbで代替します。

Q. コンテナを再起動したらデータは消えますか?

docker restartdocker stopstartでは消えません。消えるのはdocker rmでコンテナを削除して作り直したときで、しかも-vでボリュームを指定していない場合だけです。

Q. パスワードを変えたのに反映されません

既存のボリュームがあるためです。MYSQL_ROOT_PASSWORDはデータディレクトリが空の初回起動時にしか読まれません。開発環境ならdocker volume rmで作り直し、データを残したいならALTER USERで変更してください。

Q. 本番環境でDBをDockerで動かしてもよいですか?

可能ですが、ボリュームのバックアップ、監視、フェイルオーバーを自前で設計する必要があります。開発・検証環境ではDockerが圧倒的に手軽で、本番はマネージドサービスという使い分けが一般的です。

Q. -p 3306:3306で公開しても大丈夫ですか?

-p 3306:3306全てのネットワークインターフェースで待ち受けます(実測で0.0.0.0:13306と表示されたとおりです)。グローバルIPを持つサーバーでこれをやると、インターネットからMySQLに到達できてしまいます。手元からだけ使うなら-p 127.0.0.1:3306:3306のようにアドレスを限定してください。

Q. 起動を待つ処理をシェルスクリプトで書くには?

ログを条件にしたuntilループが確実です。mysqladmin pingを使う場合は必ず-h 127.0.0.1を付けてください。

until docker exec mysql-db mysqladmin ping -h 127.0.0.1 --silent; do
  echo "MySQLの起動を待っています..."
  sleep 2
done

まとめ

DockerでMySQLを動かすこと自体はコマンド1行です。つまずくのは起動した後の挙動で、その多くは「初期化は初回だけ」「Upと接続可能は別」という2つの原則から説明できます。実測して分かったことをまとめます。

  • docker psUpでも接続できない。実測で8〜13秒かかり、その間はERROR 2003 (111)が返る
  • ログのready for connectionsは2回出る。本番サーバーの目印はport: 3306(1回目はport: 0の一時サーバー)
  • 初期化中のAccess deniedはパスワードの誤りではない。一時サーバーに当たっているだけなので待てば解決する
  • healthcheckは-h 127.0.0.1でTCPを検査する。ソケット検査だと一時サーバーに反応して3秒早くhealthyになる
  • mysqladmin pingパスワードが間違っていてもexit=0。認証の検証には使えない
  • -vを付けないとdocker rmでデータが消える。付ければ残り、2回目の起動は12秒→3秒に短縮される
  • ボリュームが既にあるとMYSQL_ROOT_PASSWORDinitdb.dのSQLも完全に無視される
  • MySQL 8.0の文字コードはutf8mb4で対応不要。直すべきはタイムゾーン(既定はUTC)
  • depends_oncondition: service_healthyとセットで書く。素のdepends_onでは起動順しか守られない

まずはdocker runで1つ起動し、docker logsport: 3306の行を探すところから試してみてください。この1行が読めるようになると、接続できない原因のほとんどは自力で切り分けられます。

関連記事

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする

コメント