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なのに接続できない、パスワードを変えたのに反映されない、コンテナを作り直したらデータが消えた——この記事はそこを実測で潰していきます。
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_PASSWORD | rootのパスワード | コンテナが起動せず終了 |
-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 psのUp表示は「コンテナのプロセスが動いている」ことしか保証しません。
「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 connectionsが2回出ています。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 2003やERROR 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
興味深いのは@@portが3306と返ってくる点です。-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
テーブルごと消えました。appdbはMYSQL_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_PASSWORD・MYSQL_DATABASE・MYSQL_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 鈴木
dbがHealthyになってから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を推奨します。latestはdocker 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 restartやdocker stop→startでは消えません。消えるのは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 psがUpでも接続できない。実測で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_PASSWORDもinitdb.dのSQLも完全に無視される - MySQL 8.0の文字コードは
utf8mb4で対応不要。直すべきはタイムゾーン(既定はUTC) depends_onはcondition: service_healthyとセットで書く。素のdepends_onでは起動順しか守られない
まずはdocker runで1つ起動し、docker logsでport: 3306の行を探すところから試してみてください。この1行が読めるようになると、接続できない原因のほとんどは自力で切り分けられます。
関連記事
- docker – コンテナ/イメージを操作する Docker CLI の基底コマンド –
run・exec・logsなど各サブコマンドの詳細 - docker compose – マルチコンテナアプリを定義・実行する –
compose.ymlの書き方とサブコマンド一覧 - Dockerとは?初心者向け完全ガイド:コンテナの基礎から環境構築・デプロイまで – イメージとコンテナの関係から学びたい方へ
- mysql – MySQLサーバーへ接続してSQLを実行する対話/非対話クライアント –
-h・-e・--batchなどクライアント側のオプション - mysqldump – MySQL/MariaDBの論理バックアップを作成するダンプツール –
--single-transactionなど実務で使うオプション - クラウド時代のDBバックアップ入門|mysqldump+S3+ライフサイクル – 取ったダンプをどこに置き、どう世代管理するか
- Linuxのポート確認方法とポート競合の解消手順|netstat・lsof・ss完全ガイド –
port is already allocatedの切り分け - ミドルウェアとは?種類・具体例とサーバーでの確認コマンドを解説 – DBがシステム全体のどこに位置するか
- レプリケーションとは?バックアップとの違いをMySQLの実測で解説 – この構成を2台に増やしてレプリケーションを組む

コメント