PHP PDO「SQLSTATE[HY000] [2002] Connection refused」エラーの修正方法

intermediate🐘 PHP2026-03-19| PHP 7.4〜8.x(pdo_mysql拡張)、MySQL 5.7/8.0またはMariaDB 10.x、Linux(Ubuntu 20.04/22.04、Debian、CentOS/RHEL)、Docker/コンテナ環境でも再現可能

Error Message

Fatal error: Uncaught PDOException: SQLSTATE[HY000] [2002] Connection refused
#php#pdo#データベース#接続

エラーの内容

Fatal error: Uncaught PDOException: SQLSTATE[HY000] [2002] Connection refused
Stack trace:
#0 /var/www/html/db.php(5): PDO->__construct('mysql:host=loca...', 'root', '***')
#1 {main}
  thrown in /var/www/html/db.php on line 5

アプリが500エラーを返し続け、ログが大量に吐き出され、リクエストが積み上がっている状態です。このエラーは、PHPがMySQLへのTCP接続を一切開けない状態を意味します――認証に到達する前に弾かれています。以下の手順を順番に確認してください。

原因

SQLSTATE[HY000] [2002] はクライアント側のネットワークエラーです。MySQLクライアントが接続を試みたところ、拒否されました。実際の現場で起きる原因の95%は以下の5つに集約されます。

  • MySQL/MariaDBサービスが停止またはクラッシュしている
  • PDO DSN文字列のホストまたはポートが間違っている
  • localhost127.0.0.1 のUnixソケット問題
  • MySQLの bind-address がアプリのIPからの接続をブロックしている
  • ファイアウォールルールがポート3306のトラフィックを遮断している

手順1: MySQLは実際に動いているか?

深夜2時に最もよくある原因――サービスがクラッシュしていて誰も気づいていないケースです。まずここを確認してください。

sudo systemctl status mysql
# MariaDB
sudo systemctl status mariadb

failed または inactive と表示されている場合は起動してください。

sudo systemctl start mysql
sudo systemctl status mysql

起動に失敗する場合は、何もいじる前にログを確認してください。

sudo journalctl -u mysql -n 100 --no-pager
# または
sudo tail -100 /var/log/mysql/error.log

MySQLが起動時に落ちる原因は主に3つです。ディスク容量の不足(df -h で確認――ディスク100%が最多原因です)、InnoDBファイルの破損、または別プロセスがすでにポート3306を使用している(sudo ss -tlnp | grep 3306)。根本原因を修正してから次へ進んでください。

手順2: PDO DSNを確認する

ホストのタイポ、ポートの誤り、環境変数が空のいずれの場合も [2002] が発生します。DSNが正しいと決めつける前に、実際の実行時の値を出力して確認してください。

<?php
// アプリが実際に使っている接続パラメータを出力する
$host   = $_ENV['DB_HOST']   ?? getenv('DB_HOST')   ?: '(not set)';
$port   = $_ENV['DB_PORT']   ?? getenv('DB_PORT')   ?: '3306';
$dbname = $_ENV['DB_NAME']   ?? getenv('DB_NAME')   ?: '(not set)';
$user   = $_ENV['DB_USER']   ?? getenv('DB_USER')   ?: '(not set)';

echo "DSN will be: mysql:host={$host};port={$port};dbname={$dbname}\n";
echo "User: {$user}\n";

よくある3つのミス: DB_HOST が誤ったサーバーに解決されている(または解決されない)。ポートが間違っている――MySQLのデフォルトは3306ですが、Docker環境では3307や33060にリマップされていることがよくあります。または .env ファイルが読み込まれておらず、すべての変数が空になってDSNが mysql:host=;port=3306;dbname= になっている。

手順3: localhost と 127.0.0.1 のソケット問題

PHP開発者がよくはまる落とし穴です。PDO DSNで host=localhost を指定すると、MySQLクライアントライブラリはTCPを無視して、代わりにUnixソケットファイル経由で接続しようとします。PHPとMySQLでソケットのパスが一致しない場合、MySQLが正常に動いていても [2002] が発生します。

// Unixソケットを使用――ソケットパスが一致しないと無言で失敗する
$dsn = 'mysql:host=localhost;dbname=myapp';

// TCPを強制使用――ほぼすべての環境で確実に動作する
$dsn = 'mysql:host=127.0.0.1;dbname=myapp';

PHPとMySQLのソケットパスが一致しているか確認してください。

# PHPのpdo_mysqlがソケットを探す場所
php -r "echo ini_get('pdo_mysql.default_socket') . PHP_EOL;"

# MySQLが実際にソケットを作成する場所
grep -E "^socket" /etc/mysql/mysql.conf.d/mysqld.cnf
# または
mysqladmin -u root -p variables 2>/dev/null | grep socket

パスが一致しない場合は、DSNを 127.0.0.1 に変更するのが最も手早い解決策です。あるいは、php.inipdo_mysql.default_socket をMySQLの実際のソケットパスに合わせて設定し、PHP-FPMをリロードしてください(sudo systemctl reload php8.2-fpm)。

手順4: MySQLのbind-addressを確認する

アプリサーバーとDBサーバーが別々になっている場合や、各コンテナが独自のネットワーク名前空間を持つDocker環境の場合は、bind-address が問題になることがあります。

# MySQLがどのインターフェースでリッスンしているか確認
grep bind-address /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/my.cnf 2>/dev/null

# ssコマンドで確認
sudo ss -tlnp | grep 3306

bind-address = 127.0.0.1 の場合、MySQLはローカル以外のすべての接続を拒否します。リモートアクセスを許可するよう変更してください。

# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 0.0.0.0
sudo systemctl restart mysql

次に、アプリサーバーのIPからの接続を許可するMySQLユーザー権限を設定します。構文はバージョンによって異なります。

-- MySQL 5.7以前
GRANT ALL PRIVILEGES ON myapp.* TO 'myuser'@'APP_SERVER_IP' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;

-- MySQL 8.0以降: 先にユーザーを作成してからGRANT
CREATE USER IF NOT EXISTS 'myuser'@'APP_SERVER_IP' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON myapp.* TO 'myuser'@'APP_SERVER_IP';
FLUSH PRIVILEGES;

手順5: ファイアウォールを確認する

# UFW
sudo ufw status verbose

# iptables
sudo iptables -L -n | grep 3306

# アプリサーバーから(DBサーバーからではなく)到達可能かテスト
nc -zv DB_SERVER_IP 3306
# または
telnet DB_SERVER_IP 3306

接続が拒否されるかタイムアウトする場合は、インターネット全体に開放せず、アプリサーバーのIPに限定したファイアウォールルールを追加してください。

sudo ufw allow from APP_SERVER_IP to any port 3306
sudo ufw reload

手順6: Docker / コンテナ環境

アプリとMySQLを別々のコンテナで動かしている場合、localhost127.0.0.1 はどちらもコンテナ自身を指します――隣のMySQLコンテナではありません。docker-compose.yml で定義したサービス名を使用してください。

<?php
// 'mysql' はdocker-compose.ymlのサービス名
$dsn = 'mysql:host=mysql;port=3306;dbname=myapp';
# コンテナ同士が実際に通信できるか確認
docker exec app-container nc -zv mysql 3306

# MySQLコンテナが正常かどうか確認
docker ps | grep mysql
docker logs mysql-container-name --tail 50

修正の確認

変更後は、このスクリプトをサーバー上で直接実行してください。ブラウザの出力は信用しないでください――キャッシュされていたり、キューに溜まっていたり、別のインスタンスにルーティングされている可能性があります。

<?php
$dsn  = 'mysql:host=127.0.0.1;port=3306;dbname=myapp';
$user = 'myuser';
$pass = 'mypassword';

try {
    $pdo = new PDO($dsn, $user, $pass, [
        PDO::ATTR_ERRMODE  => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_TIMEOUT  => 5,
    ]);
    echo "Connected OK\n";
    echo "Query OK: " . $pdo->query('SELECT 1')->fetchColumn() . "\n";
} catch (PDOException $e) {
    echo "FAILED: " . $e->getMessage() . "\n";
}
php /tmp/test_db.php

期待する結果: Connected OK に続いて Query OK: 1 が表示されることです。まだ [2002] が出る場合は、エラーメッセージをよく読んでください――PHPが接続しようとした正確なホストとポートが含まれているはずです。それがDSNの誤りを特定する手がかりになります。

再発防止のポイント

  • 必ず PDO::ATTR_TIMEOUT を設定する: 設定しないと、MySQLサーバーが応答しない場合にPHPワーカーが無期限にブロックされ、アプリ全体が落ちます。5秒が無難なデフォルト値です――フリーズするより速く失敗させる設計にしましょう。
  • DSNでは localhost の代わりに 127.0.0.1 を使う。Unixソケットを使う特別な理由がない限り、これを推奨します。
  • /healthz エンドポイントを追加するSELECT 1 を実行してHTTP 200または503を返すエンドポイントを設置しておくと、ユーザーにエラーが見える前にロードバランサーがそのインスタンスをプールから外せます。
  • systemdでMySQLの自動再起動を有効にする: MySQLサービスユニットに Restart=on-failureRestartSec=5 を追加してください。一時的なクラッシュから誰も起こさずに復旧できます。
  • 稼働監視ツールでポート3306を監視する――怒ったユーザーからの報告で気づくより、1分以内のアラートの方がはるかにましです。

Related Error Notes