Lỗi Gặp Phải
Bạn khởi động Java server. Nó chết ngay lập tức với thông báo:
Exception in thread "main" java.net.BindException: Address already in use: bind
at sun.nio.ch.Net.bind0(Native Method)
at sun.nio.ch.Net.bind(Net.java:461)
at sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:223)
at io.netty.channel.socket.nio.NioServerSocketChannel.doBind(NioServerSocketChannel.java:134)
Spring Boot hiển thị lỗi này theo dạng thân thiện hơn một chút:
***************************
APPLICATION FAILED TO START
***************************
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.
Port mà server của bạn muốn dùng đã bị chiếm. Hệ điều hành áp dụng quy tắc nghiêm ngặt một chủ sở hữu: mỗi port chỉ cho một process, không có ngoại lệ.
Nguyên Nhân
Có một vài nguyên nhân phổ biến gây ra điều này:
- Một instance trước đó của server không thoát sạch và vẫn đang chạy nền — đây là thủ phạm số 1.
- Thứ gì đó khác đã chiếm port trước: một Tomcat khác, một Spring Boot app thứ hai, hoặc thậm chí một database (PostgreSQL mặc định dùng 5432, MySQL dùng 3306, Redis dùng 6379).
- Bạn đang dùng Docker và port mapping của container xung đột với thứ gì đó trên host.
- Socket đang ở trạng thái
TIME_WAIT. Process trước đã thoát, nhưng kernel đang giữ port trong tối đa 60 giây để bắt các gói tin lạc.
Cách Sửa: Tìm và Kill Process Đang Chiếm Port
Linux / macOS
Bắt đầu bằng cách xem thứ gì đang dùng port 8080:
# Dùng lsof
lsof -i :8080
# Dùng ss (nhanh hơn trên Linux hiện đại)
ss -tlnp | grep 8080
# Dùng netstat
netstat -tulnp | grep 8080
Bạn sẽ thấy kết quả tương tự như:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 12345 ubuntu 42u IPv6 123456 0t0 TCP *:8080 (LISTEN)
PID 12345 chính là thủ phạm. Kill nó đi:
kill -9 12345
Muốn dùng một lệnh duy nhất mà không cần tra PID?
fuser -k 8080/tcp
Windows
netstat -ano | findstr :8080
Lấy PID từ cột cuối cùng, rồi force-kill nó:
taskkill /PID 12345 /F
Hoặc bỏ qua hai bước này bằng PowerShell:
Get-Process -Id (Get-NetTCPConnection -LocalPort 8080).OwningProcess | Stop-Process -Force
Cách Sửa: Đổi Port Của Server
Không thể hoặc không muốn kill process kia? Chuyển app của bạn sang port khác.
Spring Boot (application.properties)
server.port=8081
Spring Boot (application.yml)
server:
port: 8081
Spring Boot (dòng lệnh — không cần thay đổi config)
java -jar myapp.jar --server.port=8081
Embedded Tomcat (lập trình thủ công)
Connector connector = new Connector();
connector.setPort(8081);
tomcat.getService().addConnector(connector);
Plain Java ServerSocket
// Chỉ định một port rảnh cụ thể
ServerSocket serverSocket = new ServerSocket(8081);
// Hoặc để OS tự chọn — truyền 0 và hỏi xem được port nào
ServerSocket serverSocket = new ServerSocket(0);
int assignedPort = serverSocket.getLocalPort();
System.out.println("Server started on port: " + assignedPort);
Port 0 ít được dùng tới. OS tự chọn một port rảnh, không thể có xung đột.
Cách Sửa: Bật SO_REUSEADDR
Port bị kẹt ở trạng thái TIME_WAIT? SO_REUSEADDR là giải pháp. Đặt nó trước khi gọi bind() và socket sẽ bỏ qua thời gian chờ hoàn toàn:
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true); // phải đặt TRƯỚC khi bind()
serverSocket.bind(new InetSocketAddress(8080));
Netty, Tomcat và Jetty đều tự động bật flag này. Bạn chỉ cần tự cấu hình nếu đang xây dựng một raw server từ đầu.
Sửa Lỗi Trên Docker
Đang chạy trong Docker? Xung đột có thể nằm ở host, không phải bên trong container:
# Xem những container nào đang map với port 8080
docker ps
lsof -i :8080
# Dừng container bị xung đột
docker stop <container_id>
# Hoặc đơn giản là map sang host port khác
docker run -p 8081:8080 myapp
Lệnh cuối giữ nguyên app lắng nghe trên port 8080 bên trong container. Host chỉ truy cập nó qua port 8081 từ bên ngoài.
Xác Nhận Đã Sửa Xong
Trước khi khởi động lại, hãy xác nhận port thực sự đã trống:
# Linux/macOS
lsof -i :8080
# Không có output = port đã rảnh
# Windows
netstat -ano | findstr :8080
# Không có output = port đã rảnh
Khởi động server của bạn và tìm thông báo bind thành công — đại loại như Started Application in 2.4 seconds hoặc Server started on port 8080. Nếu thấy thông báo đó, bạn đã xong.
Phòng Tránh
- Tắt đúng cách: Dùng SIGTERM hoặc Ctrl+C — đừng chỉ đóng terminal. Đóng terminal có thể làm JVM process bị mồ côi, và process đó tiếp tục giữ port. Hãy đăng ký một shutdown hook nếu app của bạn chưa có.
- Dùng port 0 trong test: Bind vào một port cố định trong integration test là tự rước họa vào thân, đặc biệt trong CI khi nhiều job chạy song song. Dùng
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)và để Spring tự chọn port rảnh. - Kiểm tra trước khi khởi động: Thất bại sớm trước khi JVM thậm chí khởi động:``` lsof -i :8080 && echo "Port in use!" && exit 1
- **Xung đột subnet/mạng trong microservices**: Nếu bạn đang chạy nhiều service và gặp bind error trên IP cụ thể thay vì port, có thể bạn có các dải địa chỉ bị chồng lấp. [Subnet Calculator trên ToolCraft](https://toolcraft.app/en/tools/developer/ip-subnet-calculator) có thể giúp bạn kiểm tra phân bổ mạng — chạy hoàn toàn trên trình duyệt, không upload gì cả.

