本記事は TiDB Advent Calendar 2025 の12/24の記事です。
はじめに
こんにちは、SREエンジニアの†satoken†です。普段はクラウドインフラサービスの構築・運用や業務効率化を中心に取り組んでいます。退勤した後や休日は技術カンファレンスや勉強会に参加するのが趣味で、ここ3年で参加したイベントは100回を超えていることに最近気づきました。また、TiDBのユーザーグループの運営メンバーとして、数ヶ月に1回の頻度でMeetUpイベントやハンズオンイベントを主催しております
近年、ゲーム業界においてTiDBが注目度が高まっており、採用事例も着実に増えてきました!
そこで、TiDBを採用したプロダクトの開発体制について考えてみたいと考えてみたいと思います。
最近の開発では、Dockerコンテナ化したフロントエンド、バックエンド、ミドルウェアのそれぞれの設定ファイルをチーム内で共有して進める会社も多いと思います。
TiDBはミドルウェアのデータベースになりますが、TiDBがMySQL互換であるため、MySQLサーバーで開発することも可能です。
しかし、そのような状況でTiDBのアーキテクチャを考慮せずにテーブル設計や機能開発を進めた場合、クラウド環境で初めてデータベースのマイグレーションエラーに遭遇したり、本番環境でパフォーマンスが悪化したりする可能性があり、場合によっては致命的な問題に発展することも考えられます。
そのため、ソースコードの開発時からTiDBのアーキテクチャを意識できるよう、ローカルでTiDBを起動したいと個人的に考えています。
私がローカルの開発環境構築で求めるのは、以下の要件です。
- すぐに環境を削除したり再構築でき、トライアンドエラーが簡単にできる
- TiDBに詳しくないメンバーでも簡単に起動できる技術スタックである
- 設定ファイルが少ない
今回はその要件を満たす構成をまとめてみました。
本記事は、TiDBをDockerで起動し、Laravelアプリから接続を切り替える手順の例を紹介します。
注)単純にローカルにTiDBを構築するのならtiup playgroundで起動するのが手っ取り早いので、こちらも試していただけたらと思います。(公式ページ)
概要
- TiDBをDockerで起動するためのdocker-compose.ymlと、それを用いた起動手順を紹介します
- PHPのウェブフレームワークの1つであるLaravelにおいて、ローカルで起動したTiDBと接続する手順を紹介します
本記事の対象者
- MySQL、Laravel、docker-composeで開発環境構築したことがある方
注意事項
- 本記事の設定ファイルはそのまま使用していただければ動く想定ですが、サンプルとして記載しておりTiDBの各ノードの設定などは最適化しておりません
詳細
今回の実行環境
今回実行した環境は以下の通りです。ご参考ください。
端末はMacOS 15.6.1を使用しています。
$ mysql --version
mysql Ver 8.0.44 for macos15.7 on arm64 (Homebrew)
$ docker --version
Docker version 24.0.2, build cb74dfc
$ docker-compose version
Docker Compose version v2.18.1
# Laravel用
$ php --version
PHP 8.4.10 (cli) (built: Jul 2 2025 02:22:42) (NTS)
Copyright (c) The PHP Group
Built by Homebrew
Zend Engine v4.4.10, Copyright (c) Zend Technologies
with Zend OPcache v8.4.10, Copyright (c), by Zend Technologies
$ composer --version
Composer version 2.8.10 2025-07-10 19:08:33
PHP version 8.4.10 (/opt/homebrew/Cellar/php/8.4.10/bin/php)
Run the "diagnose" command to get more detailed diagnostics output.
TiDBをDockerで起動する
ターミナルで作業用のフォルダを作成します
$ mkdir -p ~/workspace/tidb-docker
$ cd ~/workspace/tidb-docker
TiDB起動用のdocker-compose.ymlファイルを作成します。
$ touch docker-compose.yml
ファイル内容は以下の通りです。
各serviceのcommandを確認すると、互いの関係が把握できます。万が一エラーが発生した場合は、その関係を意識して設定を確認してみてください。
services:
pd0:
image: pingcap/pd:${TIDB_VERSION:-v8.5.0}
container_name: tidb-pd0
ports:
- "${PD0_CLIENT_PORT:-2379}:2379"
- "${PD0_PEER_PORT:-2380}:2380"
volumes:
- pd0_data:/data
- ./config/pd.toml:/pd.toml:ro
command:
- --name=pd0
- --client-urls=http://0.0.0.0:2379
- --peer-urls=http://0.0.0.0:2380
- --advertise-client-urls=http://pd0:2379
- --advertise-peer-urls=http://pd0:2380
- --initial-cluster=pd0=http://pd0:2380
- --data-dir=/data
- --config=/pd.toml
- --log-file=
networks:
- tidb-network
restart: unless-stopped
tikv0:
image: pingcap/tikv:${TIDB_VERSION:-v8.5.0}
container_name: tidb-tikv0
ports:
- "${TIKV0_PORT:-20160}:20160"
volumes:
- tikv0_data:/data
- ./config/tikv.toml:/tikv.toml:ro
command:
- --addr=0.0.0.0:20160
- --advertise-addr=tikv0:20160
- --data-dir=/data
- --pd=pd0:2379
- --config=/tikv.toml
- --log-file=
networks:
- tidb-network
depends_on:
- pd0
restart: unless-stopped
tikv1:
image: pingcap/tikv:${TIDB_VERSION:-v8.5.0}
container_name: tidb-tikv1
ports:
- "${TIKV1_PORT:-20161}:20160"
volumes:
- tikv1_data:/data
- ./config/tikv.toml:/tikv.toml:ro
command:
- --addr=0.0.0.0:20160
- --advertise-addr=tikv1:20160
- --data-dir=/data
- --pd=pd0:2379
- --config=/tikv.toml
- --log-file=
networks:
- tidb-network
depends_on:
- pd0
restart: unless-stopped
tikv2:
image: pingcap/tikv:${TIDB_VERSION:-v8.5.0}
container_name: tidb-tikv2
ports:
- "${TIKV2_PORT:-20162}:20160"
volumes:
- tikv2_data:/data
- ./config/tikv.toml:/tikv.toml:ro
command:
- --addr=0.0.0.0:20160
- --advertise-addr=tikv2:20160
- --data-dir=/data
- --pd=pd0:2379
- --config=/tikv.toml
- --log-file=
networks:
- tidb-network
depends_on:
- pd0
restart: unless-stopped
tidb0:
image: pingcap/tidb:${TIDB_VERSION:-v8.5.0}
container_name: tidb-tidb0
ports:
- "${TIDB0_PORT:-4000}:4000"
- "${TIDB0_STATUS_PORT:-10080}:10080"
volumes:
- ./config/tidb.toml:/tidb.toml:ro
command:
- --advertise-address=tidb0
- --store=tikv
- --path=pd0:2379
- --config=/tidb.toml
- --log-file=
networks:
- tidb-network
depends_on:
- pd0
- tikv0
- tikv1
- tikv2
restart: unless-stopped
tidb1:
image: pingcap/tidb:${TIDB_VERSION:-v8.5.0}
container_name: tidb-tidb1
ports:
- "${TIDB1_PORT:-4001}:4000"
- "${TIDB1_STATUS_PORT:-10081}:10080"
volumes:
- ./config/tidb.toml:/tidb.toml:ro
command:
- --advertise-address=tidb1
- --store=tikv
- --path=pd0:2379
- --config=/tidb.toml
- --log-file=
networks:
- tidb-network
depends_on:
- pd0
- tikv0
- tikv1
- tikv2
restart: unless-stopped
networks:
tidb-network:
driver: bridge
volumes:
pd0_data:
tikv0_data:
tikv1_data:
tikv2_data:
環境変数として以下のファイル .env を用意します
$ touch .env
内容は以下の通りです
# TiDB Version
TIDB_VERSION="v8.5.0"
# PD(Placement Driver) Ports
PD0_CLIENT_PORT=2379
PD0_PEER_PORT=2380
# TiKV Ports
TIKV0_PORT=20160
TIKV1_PORT=20161
TIKV2_PORT=20162
# TiDB Ports
TIDB0_PORT=4000
TIDB0_STATUS_PORT=10080
TIDB1_PORT=4001
TIDB1_STATUS_PORT=10081
最後に各コンテナにマウントするtomlファイルを作成します。
$ mkdir config
$ touch config/pd.toml
$ touch config/tikv.toml
$ touch config/tidb.toml
各ファイルには以下のように記載してください。
pd.toml
[replication]
max-replicas = 3
leader-schedule-limit = 4
region-schedule-limit = 2048
[schedule]
max-store-down-time = "30m"
high-space-ratio = 0.7
low-space-ratio = 0.8
[log]
# ログレベル: debug, info, warn, error
level = "info"
tikv.toml
[storage]
data-dir = "/data"
[server]
grpc-concurrency = 4
grpc-raft-conn-num = 1
[raftstore]
apply-pool-size = 2
store-pool-size = 2
region-split-check-diff = "6MiB"
[rocksdb]
max-background-jobs = 4
[log]
# ログレベル:trace, debug, info, warn, error
level = "info"
tidb.toml
[performance]
max-procs = 0
tcp-keep-alive = true
[prepared-plan-cache]
enabled = true
capacity = 100
[tikv-client]
grpc-connection-count = 4
[log]
# ログレベル: debug, info, warn, error, fatal
level = "info"
ファイルが用意できたら、docker-composeコマンドで起動します。
(注:docker-composeは自動的に.envファイルを読み込みます)
$ ls -al
-rw-r--r--@ 1 sato-kenta staff 267 Dec 22 20:25 .env
drwxr-xr-x@ 5 sato-kenta staff 160 Dec 22 00:51 config
-rw-r--r--@ 1 sato-kenta staff 3207 Dec 22 21:11 docker-compose.yml
$ docker-compose up -d
エラーが発生して停止していないかをdocker-compose psコマンドで確認します。
(注:すべてのコンテナが「Up」のステータスであれば正常に起動しています。ただし、停止していなくてもエラーが発生している場合があるため、以降の操作ができない場合はdocker-compose logsコマンドでエラーログを確認してください)
$ docker-compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
tidb-pd0 pingcap/pd:v8.5.0 "/pd-server --name=p…" pd0 34 seconds ago Up 32 seconds 0.0.0.0:2379-2380->2379-2380/tcp
tidb-tidb0 pingcap/tidb:v8.5.0 "/tidb-server --adve…" tidb0 33 seconds ago Up 31 seconds 0.0.0.0:4000->4000/tcp, 0.0.0.0:10080->10080/tcp
tidb-tidb1 pingcap/tidb:v8.5.0 "/tidb-server --adve…" tidb1 33 seconds ago Up 31 seconds 0.0.0.0:4001->4000/tcp, 0.0.0.0:10081->10080/tcp
tidb-tikv0 pingcap/tikv:v8.5.0 "/tikv-server --addr…" tikv0 34 seconds ago Up 32 seconds 0.0.0.0:20160->20160/tcp
tidb-tikv1 pingcap/tikv:v8.5.0 "/tikv-server --addr…" tikv1 34 seconds ago Up 31 seconds 0.0.0.0:20161->20160/tcp
tidb-tikv2 pingcap/tikv:v8.5.0 "/tikv-server --addr…" tikv2 34 seconds ago Up 31 seconds 0.0.0.0:20162->20160/tcp
起動したTiDBにmysqlクライアントで接続します。
自分の端末では、host名をlocalhostで指定した場合に接続できなかったため、明示的に127.0.0.1にしています。
$ mysql -h 127.0.0.1 -P 4000 -u root
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2311061516
Server version: 8.0.11-TiDB-v8.5.0 TiDB Server (Apache License 2.0) Community Edition, MySQL 8.0 compatible
Copyright (c) 2000, 2025, Oracle and/or its affiliates.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| INFORMATION_SCHEMA |
| METRICS_SCHEMA |
| PERFORMANCE_SCHEMA |
| mysql |
| sys |
| test |
+--------------------+
TiDBが起動できていることを確認できました。
Laravel プロジェクトの作成
composerコマンドでLaravelのプロジェクトを作成します。
$ composer create-project laravel/laravel tidb-app
Creating a "laravel/laravel" project at "./tidb-app"
Installing laravel/laravel (v12.11.0)
コマンドの出力では以下のような表示がされ、デフォルトでマイグレーションファイルのサンプルが作成されます。
今回はこちらを利用します。
> @php -r "file_exists('database/database.sqlite') || touch('database/database.sqlite');"
> @php artisan migrate --graceful --ansi
INFO Preparing database.
Creating migration table .......................................................................................... 2.61ms DONE
INFO Running migrations.
0001_01_01_000000_create_users_table .............................................................................. 2.66ms DONE
0001_01_01_000001_create_cache_table .............................................................................. 0.95ms DONE
0001_01_01_000002_create_jobs_table ............................................................................... 3.09ms DONE
Laravelの動作確認をします。
$ cd tidb-app
$ php artisan serve
INFO Server running on [http://127.0.0.1:8000].
Press Ctrl+C to stop the server
ブラウザからアクセスしてみてエラーが出ずに下記画面が表示されることを確認します

データベース接続設定の確認
作成したプロジェクト内のファイルツリーは以下のようになっています。
$ tree -L2
.
├── .env
├── .env.sample
├── app
│ ├── Http
│ ├── Models
│ └── Providers
├── artisan
├── bootstrap
│ ├── app.php
│ ├── cache
│ └── providers.php
├── composer.json
├── composer.lock
├── config
│ ├── app.php
│ ├── auth.php
│ ├── cache.php
│ ├── database.php
,,,
config/database.php を確認します
# L19 あたり:環境変数を設定していない場合はsqlite が指定される
'default' => env('DB_CONNECTION', 'sqlite'),
# L32 あたり:connectionsの設定。デフォルトでsqlite, mysql, pgsql, sqlsrvなどが用意されている。
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
xxx
],
.envを確認します。現在はSQLiteを使用しているのでDB接続用の環境変数がコメントアウトされています
# デフォルトではsqliteを指定している
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
config/database.phpにTiDB接続用の設定を作成し、環境変数DB_CONNECTIONでそれを指定すれば良さそうです。
TiDBに接続する設定を行う
config/database.php で以下のように修正します
# L19 あたり:環境変数を設定していない場合はtidbの設定を使用するようにする
'default' => env('DB_CONNECTION', 'tidb'),
# L32 あたり:connectionsの設定にtidbを設定を追加する
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
xxx
],
'tidb' => [
'driver' => 'mysql',
'url' => env('TIDB_URL'),
'host' => env('TIDB_HOST', '127.0.0.1'),
'port' => env('TIDB_PORT', '4000'),
'database' => env('TIDB_DATABASE', 'sample_db'),
'username' => env('TIDB_USERNAME', 'root'),
'password' => env('TIDB_PASSWORD', ''),
'unix_socket' => env('TIDB_SOCKET', ''),
'charset' => env('TIDB_CHARSET', 'utf8mb4'),
'collation' => env('TIDB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => false,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('TIDB_ATTR_SSL_CA'),
]) : [],
],
.envを以下のように修正します。今回、データベース名はsample_dbとしました。
DB_CONNECTION=tidb
# 以下の環境変数を追加
TIDB_HOST=127.0.0.1
TIDB_PORT=4000
TIDB_DATABASE=sample_db
TIDB_USERNAME=root
TIDB_PASSWORD=
マイグレーションの実行
マイグレーション用のコマンドを実行します。
(注:Laravelは自動的に.envファイルを読み込みます)
$ php artisan migrate
WARN The database 'sample_db' does not exist on the 'tidb' connection.
┌ Would you like to create it? ────────────────────────────────┐
│ Yes │
└──────────────────────────────────────────────────────────────┘
INFO Preparing database.
Creating migration table .............................. 531.89ms DONE
INFO Running migrations.
0001_01_01_000000_create_users_table ........................ 3s DONE
0001_01_01_000001_create_cache_table ........................ 1s DONE
0001_01_01_000002_create_jobs_table ......................... 2s DONE
mysqlクライアントでTiDBに接続し、マイグレートできていることを確認します
$ mysql -h 127.0.0.1 -P 4000 -u root
(TiDBであることを確認する)
Server version: 8.0.11-TiDB-v8.5.0 TiDB Server (Apache License 2.0) Community Edition, MySQL 8.0 compatible
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| INFORMATION_SCHEMA |
| METRICS_SCHEMA |
| PERFORMANCE_SCHEMA |
| mysql |
| sample_db |
| sys |
| test |
+--------------------+
7 rows in set (0.01 sec)
mysql> use sample_db;
mysql> show tables;
+-----------------------+
| Tables_in_sample_db |
+-----------------------+
| cache |
| cache_locks |
| failed_jobs |
| job_batches |
| jobs |
| migrations |
| password_reset_tokens |
| sessions |
| users |
+-----------------------+
9 rows in set (0.01 sec)
mysql> desc users;
+-------------------+-----------------+------+------+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+-----------------+------+------+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| email_verified_at | timestamp | YES | | NULL | |
| password | varchar(255) | NO | | NULL | |
| remember_token | varchar(100) | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+-------------------+-----------------+------+------+---------+----------------+
8 rows in set (0.01 sec)
マイグレーションが実行できていることを確認できました。
これでローカル環境に起動したTiDBに接続するアプリが完成しました。
Laravelアプリからデータを更新できることを確認する
デフォルトではアプリに機能がないため、データ更新できるページを作成します。
今回はロジックやUIにはこだわらず、Cursorに以下のように依頼してコントローラーやビューファイルを作成してもらいました。
今のDBテーブルを活用できるページを作って下さい
追加された機能は以下の通りです。
一言依頼するだけでこれだけの機能が作成されるのは便利です。
1. コントローラー (UserController.php)
一覧表示 (index)
作成 (create, store)
詳細表示 (show)
編集 (edit, update)
削除 (destroy)
2. ルーティング (web.php)
リソースルートで全CRUD操作を定義
トップページをユーザー一覧にリダイレクト
3. ビューファイル
index.blade.php - ユーザー一覧(ページネーション対応)
create.blade.php - 新規ユーザー作成フォーム
edit.blade.php - ユーザー編集フォーム
show.blade.php - ユーザー詳細表示
機能
ユーザー一覧表示(ページネーション)
新規ユーザー作成
ユーザー編集(パスワードは任意)
ユーザー詳細表示
ユーザー削除(確認ダイアログ付き)
バリデーションエラー表示
成功メッセージ表示
ソースコードは今回の主旨ではないので割愛させていただきます
ブラウザ上からユーザー作成操作を行ってみます。
一覧画面は下のようになりました


下のように登録できました。

TiDB側を確認すると、データが登録されていることがわかります。
※ パスワードの箇所は念の為マスクしてあります
mysql> select * from users;
+----+--------------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
| id | name | email | email_verified_at | password | remember_token | created_at | updated_at |
+----+--------------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
| 1 | さとけん | sample@example.test | NULL | xxx | NULL | 2025-12-22 04:00:30 | 2025-12-22 04:00:30 |
+----+--------------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
1 row in set (0.03 sec)
補足ですが、EXPLAINを実行してみると、TiDB Cloudで実行した時と同様にTaskのところにcop[tikv]と表示され、TiDBであることを実感できます。
mysql> explain select * from users;
+-----------------------+---------+-----------+---------------+--------------------------------+
| id | estRows | task | access object | operator info |
+-----------------------+---------+-----------+---------------+--------------------------------+
| TableReader_5 | 1.00 | root | | data:TableFullScan_4 |
| └─TableFullScan_4 | 1.00 | cop[tikv] | table:users | keep order:false, stats:pseudo |
+-----------------------+---------+-----------+---------------+--------------------------------+
2 rows in set (0.01 sec)
また、2人目のユーザーを作成すると、新たに作成されたユーザーのid列の値が30001になっており、TiDBの分散環境におけるAUTO_INCREMENTの特性を実感できます。
mysql> select * from users;
+-------+--------------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
| id | name | email | email_verified_at | password | remember_token | created_at | updated_at |
+-------+--------------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
| 1 | さとけん | sample@example.test | NULL | xxx | NULL | 2025-12-22 04:00:30 | 2025-12-22 04:00:30 |
| 30001 | sasa | sasas@example.test | NULL | xxx| NULL | 2025-12-22 05:18:32 | 2025-12-22 05:18:32 |
+-------+--------------+---------------------+-------------------+--------------------------------------------------------------+----------------+---------------------+---------------------+
2 rows in set (0.00 sec)
まとめ
TiDBをDockerで起動し、ローカルで起動したアプリと接続する手順を紹介しました。
docker-compose.ymlファイルは構成面でもう少し改善の余地があると思います。
データベース設定ファイルを少し変更するだけでTiDBに問題なく接続できるのは、MySQL互換性の高さを実感できる点ですね。
TiDBはMySQL互換とはいえ、パフォーマンスを十分に発揮してもらうためには、アーキテクチャを意識したテーブル設計が不可欠です。
トライアンドエラーのイテレーションを短時間で回せるよう、今回紹介したようなローカル環境を活用していきたいと思います。