DEV Community

Ayesha
Ayesha

Posted on Originally published at webixyhub.com

How to Fix SQLSTATE[HY000] [2002] Connection Refused in Laravel

How to Fix SQLSTATE[HY000] [2002] Connection Refused in Laravel

You run php artisan migrate, hit refresh in the browser, or deploy to staging — and instead of your app you get the dreaded wall of red:

SQLSTATE[HY000] [2002] Connection refused (Connection: mysql, SQL: select * from information_schema.tables ...)
Enter fullscreen mode Exit fullscreen mode

SQLSTATE[HY000] [2002] connection refused in Laravel means one thing and one thing only: PHP tried to open a network connection to your database server, and nothing answered at that host and port. Your SQL is fine. Your migrations are fine. The database simply is not reachable from where Laravel is running. This guide walks through every real-world cause I have seen in production and local development, in the order you should check them, with the exact fix for each.

What "SQLSTATE[HY000] [2002] Connection Refused" Actually Means

Break the error down piece by piece: SQLSTATE[HY000] is a generic PDO driver error code — "general error". [2002] is the MySQL client error code for "can't connect to server". "Connection refused" is the operating system telling you the TCP connection was actively rejected — or that nothing was listening on that address and port. The key insight: this error is thrown by the MySQL client library before a single byte of SQL is sent. So don't waste time reviewing your queries. The problem is always in the connection path: the host, the port, the transport (socket vs TCP), the credentials handshake, or the server simply being down.

Step 0: Reproduce It Outside Laravel

Before touching your .env file, confirm where the failure actually is. Run this from the server or machine running your app:

# 1. Is anything listening on the MySQL port?
nc -zv 127.0.0.1 3306

# 2. Can the MySQL client connect with the same credentials Laravel uses?
mysql -h 127.0.0.1 -P 3306 -u your_db_user -p your_database
Enter fullscreen mode Exit fullscreen mode

If nc says the connection was refused, your database server is down or listening elsewhere — that is a server problem, not a Laravel problem. If nc succeeds but the mysql client fails with access denied, the server is up and your credentials or user host permissions are wrong. Either way, you have just narrowed the search dramatically before editing a single line of application code.

Fix 1: The Database Server Is Not Running

The most common cause, and the most embarrassing. After a server reboot, a Docker restart, or a fresh Homestead/Vagrant provision, MySQL/MariaDB may simply not be started — or started but not enabled to start on boot.

# Check the status
sudo systemctl status mysql    # Ubuntu/Debian
sudo systemctl status mariadb  # if you run MariaDB

# Start it now AND enable it for the next reboot
sudo systemctl enable --now mysql
Enter fullscreen mode Exit fullscreen mode

The enable flag is the part everyone misses: start fixes it right now, enable fixes it after the next reboot too. If you are on macOS with a tool like DBngin or MAMP, open the app and confirm the server indicator is green — those tools don't always restart servers after a system update. Then confirm Laravel sees it:

php artisan migrate:status
Enter fullscreen mode Exit fullscreen mode

Fix 2: localhost vs 127.0.0.1 — The Socket Trap

This is the number one cause of a Laravel MySQL "connection refused" error on local machines, and it confuses everyone at least once. When you set DB_HOST=localhost, the MySQL client doesn't use TCP. It tries to connect through a Unix socket file (like /var/run/mysqld/mysqld.sock). When you set DB_HOST=127.0.0.1, it uses TCP over the loopback interface. If the socket file path the client expects doesn't match where the server actually created it, you get connection refused — even though the server is running perfectly.

The fix: in your .env, use the IP address to force TCP, which removes the socket path from the equation entirely:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_db_user
DB_PASSWORD=your_password
Enter fullscreen mode Exit fullscreen mode

After editing .env, clear the config cache (more on that in Fix 5) and retry. If you specifically want socket connections, set DB_SOCKET to the real socket path — find it with mysqladmin variables | grep socket — but for 95% of apps, TCP on 127.0.0.1 is simpler and more predictable.

Fix 3: The Port Does Not Match

MAMP defaults to port 8889, not 3306. Docker Compose mappings can publish MySQL on any host port. Laravel Sail uses 3306 on the container but your host might map something else. If your database tool connects fine on a custom port but Laravel refuses, your DB_PORT is almost certainly wrong.

DB_PORT=8889  # MAMP default — adjust to whatever your server actually uses
Enter fullscreen mode Exit fullscreen mode

Find the real port: check your DBngin/MAMP/Docker settings, or ask the server directly:

sudo ss -tlnp | grep -E 'mysql|maria'
Enter fullscreen mode Exit fullscreen mode

That shows you exactly which port the server is listening on. Match DB_PORT to it.

Fix 4: Docker and Sail Hostnames

Inside Docker, 127.0.0.1 means "this container", not your laptop and not the MySQL container. This is the classic Laravel database "connection refused" scenario in Docker Compose and Laravel Sail setups. The rule is simple: from inside a container (your app container, queue workers, sail artisan) use the service name as the host, e.g. DB_HOST=mysql. From your host machine (running php artisan locally, or a GUI client) use 127.0.0.1 with the published port.

# .env for the app running INSIDE docker (Sail default)
DB_HOST=mysql
DB_PORT=3306

# .env for the app running on your HOST talking to a dockerized database
DB_HOST=127.0.0.1
DB_PORT=3306  # the published/host port from docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Also give the database container a moment to be ready. Containers start in parallel, and Laravel can boot before MySQL finishes initializing. In CI pipelines, add a health check or a wait step; locally, just retry after 10–20 seconds. If your queue workers run in a separate long-lived container, restart them after fixing .env — they cache the old config in memory.

Fix 5: Your Config Is Cached (The Silent Killer)

You fixed .env, restarted everything, triple-checked the port — and the error persists. Nine times out of ten, Laravel is not reading your .env at all because the configuration is cached. php artisan config:cache bakes every value into bootstrap/cache/config.php, and after that, .env edits are silently ignored.

php artisan config:clear
php artisan cache:clear
Enter fullscreen mode Exit fullscreen mode

Then verify what Laravel actually sees:

php artisan tinker --execute="print_r(config('database.connections.mysql'));"
Enter fullscreen mode Exit fullscreen mode

If the output doesn't match your current .env, the cache was the problem. Two rules to live by:

  1. Never run config:cache in local development. It exists for production performance.
  2. In production, your deploy script should run config:clear then config:cache (in that order) on every deploy, and restart queue workers with php artisan queue:restart so they pick up the fresh config.

This one cause alone accounts for a huge share of "but I already fixed the .env!" support threads.

Fix 6: Remote Databases — Bind Address, Users, and Firewalls

If your database lives on a different server (a managed database, a separate droplet, RDS), connection refused usually means the network path is blocked:

# On the DATABASE server, check what MySQL is bound to
sudo grep -R "bind-address" /etc/mysql/
Enter fullscreen mode Exit fullscreen mode

If bind-address = 127.0.0.1, the server only accepts local connections. For remote access it needs the server's private IP or 0.0.0.0 (then restart MySQL). Next, check the MySQL user's allowed host:

SELECT user, host FROM mysql.user;
Enter fullscreen mode Exit fullscreen mode

A user created as 'app'@'localhost' can never connect from another machine — you need 'app'@'%' or 'app'@'your-app-server-ip'. Finally, check the firewall:

sudo ufw status

# allow MySQL from your app server only, never the whole internet
sudo ufw allow from 203.0.113.10 to any port 3306
Enter fullscreen mode Exit fullscreen mode

Opening port 3306 to the world is a security incident waiting to happen. Restrict it to your app server's IP, or better, keep the database on a private network.

The 2-Minute Diagnostic Sequence

When this error appears and you don't know the cause yet, run these in order. Most cases resolve by step 3:

# Step 1: what does Laravel THINK the connection settings are?
php artisan tinker --execute="print_r(config('database.connections.mysql'));"

# Step 2: is anything listening where Laravel is pointing?
nc -zv 127.0.0.1 3306

# Step 3: can the mysql client connect with those same settings?
mysql -h 127.0.0.1 -P 3306 -u your_db_user -p your_database -e "SELECT 1;"

# Step 4: still failing? nuke the caches and restart long-lived processes
php artisan config:clear && php artisan cache:clear && php artisan queue:restart
Enter fullscreen mode Exit fullscreen mode

Add this as a snippet to your team's runbook. It turns a 45-minute panic into a 2-minute routine.

Common Pitfalls Checklist

  • Editing .env on the wrong server. In Forge/Vapor/CI setups, make sure you edited the environment of the machine actually running the app.
  • DB_HOST set to a container name on the host, or 127.0.0.1 inside a container. Match the host value to where the code runs (Fix 4).
  • Typos in DB_DATABASE. A database that doesn't exist gives a different error ("unknown database"), but a typo'd username gives access denied — read the message carefully instead of assuming connection refused.
  • SELinux or AppArmor blocking the web server's outbound connections on hardened servers — check audit.log if everything else looks right.
  • Running php artisan serve in one terminal and editing .env in another — the dev server does pick up .env changes per request, but queue workers and Octane do not. Restart them.
  • Assuming the fix worked without re-running the failing command. Always re-run php artisan migrate:status or reload the page to confirm.

Prevent It From Coming Back

Add database connectivity to your deployment and monitoring so you catch this before your users do:

// routes/console.php — a scheduled health check
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    DB::connection()->getPdo(); // throws on connection failure
})->everyFiveMinutes()->name('db-health-check');
Enter fullscreen mode Exit fullscreen mode

Pair that with a deploy script that clears and rebuilds the config cache on every release, and this error becomes something you read about in other people's blog posts instead of debugging at 2 AM.

Conclusion

Connection refused is Laravel telling you the truth: nothing answered at the address you gave it. Work the connection path — server, host, port, cache — and the fix is almost always one config line or one service start away. If your team is losing hours to Laravel database issues, deployment headaches, or an API that keeps falling over, I help founders and teams ship stable Laravel applications. See my work and get in touch at WebixyHub.


Originally published at https://webixyhub.com/blog/fix-sqlstate-hy000-2002-connection-refused-laravel

Top comments (0)