Fixing SQLSTATE[HY000] [2002] Connection Refused in Laravel (Local, Docker & Sail)


Nothing derails a backend sprint faster than spinning up a Laravel project, running php artisan migrate, and getting hit with a wall of red terminal text:

Illuminate\Database\QueryException

SQLSTATE[HY000] [2002] Connection refused (Connection: mysql, SQL: select * from information_schema.tables where table_schema = app_db and table_name = migrations and table_type = 'BASE TABLE')

Unlike authentication errors (1045 Access denied) or unknown database errors (1049), error code 2002 (Connection refused) happens at the networking layer before MySQL even checks your username or password. It means PHP’s PDO driver knocked on a specific IP address and port, and the operating system actively rejected the TCP handshake because nothing was listening there.

Having debugged this across dozens of local setups (Laravel Herd, DBngin, XAMPP) and containerized staging servers, this error almost always comes down to one of five root causes.


Quick Diagnostic Reference

If you just want to get back to coding, match your current setup to the exact cause below:

Environment Typical Cause Immediate Fix
Native Local (Herd / Valet / MAMP) Unix socket vs TCP loopback mismatch Set DB_HOST=127.0.0.1 instead of localhost
Laravel Sail / Docker Compose Container loopback isolation Set DB_HOST=mysql inside the container
Host CLI + Docker DB Running php artisan outside Docker Use ./vendor/bin/sail artisan or DB_HOST=127.0.0.1
Custom Local Setup (DBngin / Brew) Port collision on 3306 Verify port in TablePlus/DBngin (3306 vs 3307)
CI/CD or Fresh Docker Boot MySQL daemon still initializing Wait for health check before running migrations

1. The 127.0.0.1 vs. localhost Socket Trap

In most networking contexts, localhost and 127.0.0.1 are interchangeable. In PHP’s MySQL PDO driver, they are not.

  • When you set DB_HOST=localhost, PHP ignores TCP/IP networking entirely and tries to connect through a local Unix domain socket file (usually /tmp/mysql.sock or /var/run/mysqld/mysqld.sock).
  • When you set DB_HOST=127.0.0.1, PHP forces a standard TCP/IP network connection over port 3306.

If you use tools like DBngin, Docker, or a standalone MySQL server where the socket file isn’t in PHP’s default pdo_mysql.default_socket path, using localhost fails immediately.

How to fix it:

Open your .env file and explicitly use the IPv4 loopback address:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_db_name
DB_USERNAME=root
DB_PASSWORD=

Then clear your cached configuration so Laravel actually reads the updated .env file:

php artisan config:clear

Note on Config Caching: If you previously ran php artisan config:cache during testing, Laravel completely ignores your .env file until you run config:clear. Always clear your config cache after changing database credentials.


2. Docker & Laravel Sail Networking Mistakes

If you are running your app inside Docker or Laravel Sail, networking works differently. Every container gets its own isolated virtual network interface.

If your PHP container has DB_HOST=127.0.0.1, PHP looks for MySQL inside the PHP container itself—where MySQL doesn’t exist.

Scenario A: Running commands inside the container

When your web app runs inside Docker (or you run commands via sail artisan migrate), DB_HOST must match the service name defined in your docker-compose.yml file:

services:
  laravel.test:
    # Your PHP application container
    depends_on:
      - mysql
  mysql:
    image: 'mysql/mysql-server:8.0'
    ports:
      - '${FORWARD_DB_PORT:-3306}:3306'

Your .env needs to target that exact service name (mysql):

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=sail
DB_PASSWORD=password

Scenario B: Running php artisan from your host machine’s terminal

A classic trap happens when your database runs inside Docker (DB_HOST=mysql), but you accidentally type php artisan migrate directly in your local Windows PowerShell or macOS terminal instead of going through Docker. Your host machine has no idea what the hostname mysql is, or refuses the connection.

The solution: Always route your Artisan commands through the container:

# If using Laravel Sail
./vendor/bin/sail artisan migrate

# If using standard Docker Compose
docker compose exec app php artisan migrate

3. MySQL Service Stopped or Crashed Out of Memory

Sometimes the configuration is fine, but the MySQL daemon simply isn’t running. On smaller VPS instances (like a 1GB RAM DigitalOcean droplet or AWS Lightsail instance), Linux’s OOM (Out-Of-Memory) killer will silently terminate mysqld during heavy queries or composer updates.

Check if the MySQL service is actually alive and listening on port 3306:

On Linux (Ubuntu/Debian):

sudo systemctl status mysql

If it shows inactive (dead) or failed, start it back up:

sudo systemctl start mysql

On macOS (Homebrew):

brew services list
brew services restart mysql

On Windows (PowerShell): Check if any process is actively listening on port 3306:

Get-NetTCPConnection -LocalPort 3306

If that command returns an error stating no matching MSFT_NetTCPConnection objects were found, open your local database manager (DBngin, Laragon, or Windows Services) and start the MySQL engine.


4. Silent Port Collisions (3306 vs 3307)

If you have ever installed MySQL natively (via Windows Installer, XAMPP, or Homebrew) and later started using Docker or DBngin, you likely have two different MySQL instances fighting over port 3306.

When you spin up a secondary database in DBngin or Docker to avoid a conflict, you might map it to port 3307 or 33060 on your host machine—And then forget to update your Laravel .env file.

Verify which process actually owns port 3306:

Linux / macOS:

sudo lsof -i :3306

Windows (PowerShell):

netstat -ano | findstr :3306

Check your database GUI client (like TablePlus or DBeaver). If you connect to your local database in TablePlus using port 3307, your Laravel .env must match it:

DB_HOST=127.0.0.1
DB_PORT=3307

5. Custom Unix Sockets (MAMP & Shared Hosting)

If you use MAMP on macOS or certain cPanel shared hosts, the provider often disables TCP networking for local connections and forces you to use a custom Unix socket file.

If 127.0.0.1 refuses to connect on these setups, locate the active socket path by running this command in your MySQL CLI or GUI query editor:

SHOW VARIABLES LIKE 'socket';

Once you have the exact path (for example, /Applications/MAMP/tmp/mysql/mysql.sock), add the DB_SOCKET variable to your .env file:

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sock
DB_DATABASE=your_db
DB_USERNAME=root
DB_PASSWORD=root

(Confirm that config/database.php still has 'unix_socket' => env('DB_SOCKET', '') inside the mysql connection array, which is standard in Laravel).


Still Stuck? Test the Raw Connection Outside Laravel

To prove whether the bug is inside your Laravel project or your system’s network/MySQL setup, take PHP out of the equation and test the raw TCP port from your terminal:

# Test if port 3306 is open on 127.0.0.1
nc -zv 127.0.0.1 3306
  • If nc (netcat) says Connection refused, Laravel is innocent—your MySQL server is either turned off, listening on a different port, or blocked by a firewall.
  • If nc says succeeded!, your MySQL server is running properly, and your Laravel project is almost certainly holding onto stale credentials in bootstrap/cache/config.php. Delete that file manually or run php artisan optimize:clear, and your database connection will go right through.