Tình huống
Bạn đang cố kết nối đến một MySQL server chạy trên máy khác — có thể là staging server, cloud VM, hoặc DB host chuyên dụng. Bạn chạy lệnh:
mysql -h 192.168.1.100 -u myuser -p
Và ngay lập tức nhận được thông báo:
ERROR 2003 (HY000): Can't connect to MySQL server on '192.168.1.100' (111)
Phần (111) là chi tiết quan trọng. Đó là mã lỗi Linux errno cho ECONNREFUSED — máy chủ đã từ chối kết nối TCP. Lỗi này khác với timeout (110), khi gói tin bị âm thầm bỏ qua. Có gì đó đang chặn kết nối trước khi MySQL kịp nhận, hoặc MySQL không lắng nghe trên địa chỉ đó.
Chẩn đoán trước — đừng đoán mò
Trước khi động vào bất kỳ cấu hình nào, hãy xác định chính xác kết nối đang thất bại ở đâu. Chạy các lệnh này từ máy client của bạn:
# Bạn có thể ping đến host không?
ping 192.168.1.100
# Port 3306 có mở và chấp nhận kết nối không?
nc -zv 192.168.1.100 3306
# hoặc
telnet 192.168.1.100 3306
Nếu nc báo Connection refused, nguyên nhân nằm ở firewall hoặc bind address của MySQL. Nếu bị timeout, firewall đang âm thầm drop gói tin. Nếu kết nối thành công, MySQL đã tiếp cận được — nhưng khi đó bạn sẽ thấy lỗi khác, không phải 2003.
Nguyên nhân 1: MySQL chỉ lắng nghe trên localhost
Đây là nguyên nhân phổ biến nhất. Mặc định, MySQL bind vào 127.0.0.1, nghĩa là chỉ chấp nhận kết nối từ cùng một máy. Mọi kết nối từ xa đều bị từ chối ngay lập tức.
Kiểm tra MySQL đang thực sự lắng nghe trên gì — chạy lệnh này trên DB server:
sudo ss -tlnp | grep 3306
# hoặc trên hệ thống cũ hơn:
sudo netstat -tlnp | grep 3306
127.0.0.1:3306 có nghĩa MySQL chỉ chấp nhận kết nối nội bộ — đây chính là thủ phạm. 0.0.0.0:3306 nghĩa là nó đã lắng nghe trên tất cả interfaces, hãy chuyển sang Nguyên nhân 2.
Cách sửa: Thay đổi bind-address trong my.cnf
Tìm file cấu hình MySQL — thường là /etc/mysql/mysql.conf.d/mysqld.cnf trên Ubuntu/Debian, hoặc /etc/my.cnf trên CentOS/RHEL:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
Tìm dòng bind-address và sửa lại:
[mysqld]
# Trước:
bind-address = 127.0.0.1
# Sau (lắng nghe trên tất cả interfaces):
bind-address = 0.0.0.0
Muốn lắng nghe trên một IP LAN cụ thể thay vì tất cả? Dùng IP đó thay thế:
bind-address = 192.168.1.100
Khởi động lại MySQL để áp dụng thay đổi:
sudo systemctl restart mysql
# hoặc trên hệ thống cũ hơn:
sudo service mysql restart
Xác nhận MySQL đang lắng nghe đúng địa chỉ:
sudo ss -tlnp | grep 3306
# Kết quả mong đợi: 0.0.0.0:3306 hoặc IP cụ thể của bạn
Nguyên nhân 2: Firewall chặn port 3306
MySQL có thể đang lắng nghe trên tất cả interfaces nhưng vẫn không thể kết nối được. Firewall của OS hoặc security group trên cloud có thể đang drop traffic vào port 3306 trước khi nó đến nơi.
Cách sửa trên Ubuntu/Debian (UFW)
# Cho phép từ một IP cụ thể (khuyến nghị):
sudo ufw allow from 192.168.1.50 to any port 3306
# Hoặc cho phép từ một subnet:
sudo ufw allow from 192.168.1.0/24 to any port 3306
# Kiểm tra trạng thái:
sudo ufw status
Cách sửa trên CentOS/RHEL (firewalld)
# Cho phép port MySQL:
sudo firewall-cmd --permanent --add-port=3306/tcp
sudo firewall-cmd --reload
# Hoặc cho phép từ một nguồn cụ thể:
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.50" port port="3306" protocol="tcp" accept'
sudo firewall-cmd --reload
Cách sửa trực tiếp bằng iptables
sudo iptables -A INPUT -p tcp --dport 3306 -s 192.168.1.0/24 -j ACCEPT
# Lưu cấu hình vĩnh viễn:
sudo iptables-save > /etc/iptables/rules.v4
Đang chạy trên AWS, GCP, hoặc Azure? Hãy kiểm tra thêm security groups / VPC firewall rules. Firewall của OS và firewall của cloud là hai lớp riêng biệt — tôi đã mất 20 phút sửa UFW xong mới phát hiện security group vẫn đang chặn port 3306.
Nguyên nhân 3: MySQL chưa chạy
Thật ra nên kiểm tra điều này trước tiên:
sudo systemctl status mysql
# hoặc:
sudo systemctl status mysqld
Chưa chạy? Khởi động và bật tự động khởi động cùng hệ thống:
sudo systemctl start mysql
sudo systemctl enable mysql
Nếu khởi động thất bại, log sẽ cho biết lý do:
sudo journalctl -u mysql --no-pager -n 50
# hoặc:
sudo tail -100 /var/log/mysql/error.log
Nguyên nhân 4: Sai port hoặc hostname
Port không chuẩn gây nhầm lẫn thường xuyên hơn bạn nghĩ. Kiểm tra MySQL đang dùng port nào trên server:
sudo ss -tlnp | grep mysql
Sau đó kết nối với port chỉ định rõ ràng:
mysql -h 192.168.1.100 -P 3307 -u myuser -p
Đang dùng hostname thay vì IP? Xác nhận DNS phân giải đúng:
nslookup db.example.com
# hoặc:
dig db.example.com
Sau khi sửa kết nối — Cấp quyền truy cập từ xa cho MySQL user
TCP đã thông, MySQL đang lắng nghe — nhưng bây giờ bạn nhận được Access denied. Tài khoản MySQL user được gắn với host mà chúng kết nối từ đó. Một user được tạo với cú pháp 'myuser'@'localhost' đơn giản là không thể đăng nhập từ xa, dù có đúng mật khẩu đi nữa.
Đăng nhập trực tiếp trên DB server và cấp quyền truy cập từ xa:
mysql -u root -p
-- Cho phép từ bất kỳ IP nào (ổn cho dev, rủi ro trong prod):
CREATE USER 'myuser'@'%' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON mydb.* TO 'myuser'@'%';
FLUSH PRIVILEGES;
-- Tốt hơn: giới hạn cho một IP cụ thể:
CREATE USER 'myuser'@'192.168.1.50' IDENTIFIED BY 'your_password';
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'myuser'@'192.168.1.50';
FLUSH PRIVILEGES;
Ký tự đại diện '%' cho phép kết nối từ bất kỳ IP nào. Tiện lợi cho dev nội bộ. Tuyệt đối không dùng trong môi trường production.
Xác nhận đã sửa xong
# Từ máy client — kiểm tra kết nối TCP trước:
nc -zv 192.168.1.100 3306
# Kết quả mong đợi: Connection to 192.168.1.100 3306 port [tcp/mysql] succeeded!
# Sau đó kiểm tra đăng nhập MySQL:
mysql -h 192.168.1.100 -u myuser -p mydb
# Kết quả mong đợi: MySQL prompt
TCP thông nhưng đăng nhập MySQL thất bại? Bạn đã vượt qua ERROR 2003 và đang gặp vấn đề xác thực — hãy kiểm tra lại quyền user như đã hướng dẫn ở trên.
Lưu ý bảo mật — Đừng mở port 3306 ra toàn thế giới
Luôn giới hạn firewall rules cho các IP hoặc subnet cụ thể. Việc để MySQL lắng nghe trên 0.0.0.0/0 trên một server public là tự chuốc họa — các scanner tự động sẽ tìm thấy port 3306 trong vài phút và bắt đầu dò quét. Nếu app server và DB nằm trong cùng một VPC, hãy giới hạn trong subnet đó. Để developer truy cập từ xa, SSH tunnel là lựa chọn gọn gàng và an toàn hơn:
# SSH tunnel — chuyển tiếp port local 3307 đến MySQL từ xa
ssh -L 3307:127.0.0.1:3306 user@192.168.1.100 -N -f
# Sau đó kết nối nội bộ:
mysql -h 127.0.0.1 -P 3307 -u myuser -p
MySQL hoàn toàn không cần phải lộ ra ngoài mạng.
Mẹo
Khi viết firewall rules cho nhiều subnet khác nhau, rất dễ tính sai CIDR range. Tôi dùng ToolCraft's Subnet Calculator để nhanh chóng xác nhận rằng một IP như 192.168.1.50 có thực sự nằm trong 192.168.1.0/24 hay không trước khi áp dụng rule. Công cụ chạy trên trình duyệt, không upload dữ liệu — rất tiện để kiểm tra nhanh.

