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 ...). 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 do not 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
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
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 do not always restart servers after a system update. Then confirm Laravel sees it:
php artisan migrate:status
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 does not 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 does not 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
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
Find the real port: check your DBngin/MAMP/Docker settings, or ask the server directly:
sudo ss -tlnp | grep -E 'mysql|maria'
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
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
Then verify what Laravel actually sees:
php artisan tinker --execute="print_r(config('database.connections.mysql'));"
If the output does not 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/
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;
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
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 do not 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
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 does not 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');
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 (https://webixyhub.com).