HAP
×3
HAProxy LB
round-robin • health • scale

Cách cân bằng tải với HAProxy trên Docker Compose

Hướng dẫn triển khai HAProxy làm load balancer với Docker Compose, bao gồm cấu hình round-robin, health check và Stats UI để giám sát backend.

12 phút đọc16/06/2026

Bài toán: một server chịu không nổi, nhiều server lại cần ai điều phối

Khi app bắt đầu chịu tải cao, cách đơn giản nhất là scale ngang: chạy nhiều instance cùng lúc. Nhưng ngay lập tức bạn gặp vấn đề tiếp theo:

  • Client gọi vào đâu?
  • Nếu một instance chết, request vẫn đổ vào nó thì sao?
  • Làm sao biết instance nào đang khoẻ?

Đây là lúc bạn cần một load balancer đứng trước các backend để nhận request rồi phân phối đều ra.

HAProxy là công cụ làm chuyện đó. Nó nhẹ, nhanh, cấu hình rõ ràng và đã được dùng ở production lớn từ hơn hai mươi năm nay.

Cân bằng tải là gì, giải thích ngắn

Cân bằng tải (load balancing) là kỹ thuật phân phối request đến nhiều server backend thay vì dồn vào một chỗ.

Client
  │
  ▼
Load Balancer (HAProxy)
  ├── Backend 1 (app:8001)
  ├── Backend 2 (app:8002)
  └── Backend 3 (app:8003)

HAProxy nhận toàn bộ request ở port đầu vào, sau đó quyết định chuyển request đó đến backend nào dựa trên thuật toán đã cấu hình.

Ba thuật toán phổ biến nhất:

Thuật toán Cách hoạt động Dùng khi nào
roundrobin Luân phiên theo thứ tự Request nhẹ, tương đương nhau
leastconn Gửi đến server ít connection nhất Request nặng, xử lý lâu
source Cùng IP client → cùng backend Cần session sticky

Điều kiện trước khi bắt đầu

  • Máy đã cài Docker và Docker Compose
  • Hiểu cơ bản về Docker networking
  • Có sẵn một app để test (bài này dùng Nginx làm backend giả)

Cấu trúc project

haproxy-demo/
├── docker-compose.yml
├── haproxy/
│   └── haproxy.cfg

Bước 1: Viết file cấu hình HAProxy

Tạo file haproxy/haproxy.cfg:

global
    log stdout format raw local0
    maxconn 4096

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect 5s
    timeout client  30s
    timeout server  30s

frontend http_front
    bind *:80
    default_backend web_backends

backend web_backends
    balance roundrobin
    option httpchk GET /
    http-check expect status 200
    server backend1 app1:80 check inter 5s fall 3 rise 2
    server backend2 app2:80 check inter 5s fall 3 rise 2
    server backend3 app3:80 check inter 5s fall 3 rise 2

listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 5s
    stats show-node
    stats auth admin:admin123

Giải thích các phần quan trọng:

  • frontend http_front: HAProxy lắng nghe port 80, nhận mọi request HTTP vào
  • backend web_backends: định nghĩa nhóm backend và cách phân phối
  • balance roundrobin: luân phiên đều qua 3 server
  • option httpchk GET /: health check bằng cách gửi GET đến /
  • check inter 5s fall 3 rise 2: kiểm tra mỗi 5 giây, down sau 3 lần thất bại liên tiếp, up lại sau 2 lần thành công
  • listen stats: Stats UI trên port 8404, bảo vệ bằng basic auth

Bước 2: Viết docker-compose.yml

services:
  haproxy:
    image: haproxy:2.9-alpine
    ports:
      - "80:80"
      - "8404:8404"
    volumes:
      - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on:
      - app1
      - app2
      - app3
    restart: unless-stopped

  app1:
    image: nginx:alpine
    volumes:
      - ./app1:/usr/share/nginx/html:ro

  app2:
    image: nginx:alpine
    volumes:
      - ./app2:/usr/share/nginx/html:ro

  app3:
    image: nginx:alpine
    volumes:
      - ./app3:/usr/share/nginx/html:ro

Bước 3: Tạo nội dung backend giả để phân biệt

mkdir -p app1 app2 app3
echo "<h1>Backend 1</h1>" > app1/index.html
echo "<h1>Backend 2</h1>" > app2/index.html
echo "<h1>Backend 3</h1>" > app3/index.html

Bước 4: Chạy stack

docker compose up -d

Kiểm tra tất cả container đang chạy:

docker compose ps

Kết quả mong đợi:

NAME              STATUS          PORTS
haproxy-demo-haproxy-1   Up    0.0.0.0:80->80/tcp, 0.0.0.0:8404->8404/tcp
haproxy-demo-app1-1      Up    80/tcp
haproxy-demo-app2-1      Up    80/tcp
haproxy-demo-app3-1      Up    80/tcp

Bước 5: Kiểm tra cân bằng tải

Gửi vài request liên tiếp và xem response thay đổi:

for i in $(seq 1 6); do curl -s http://localhost/; echo; done

Kết quả sẽ luân phiên:

<h1>Backend 1</h1>
<h1>Backend 2</h1>
<h1>Backend 3</h1>
<h1>Backend 1</h1>
<h1>Backend 2</h1>
<h1>Backend 3</h1>

Bước 6: Xem Stats UI

Mở trình duyệt vào http://localhost:8404/stats, đăng nhập với admin / admin123.

Stats UI cho thấy:

  • Trạng thái từng backend (UP / DOWN)
  • Số request đã xử lý
  • Số connection hiện tại
  • Số lần health check thất bại

Kiểm tra health check hoạt động đúng chưa

Tắt một backend thủ công:

docker compose stop app2

Gửi request tiếp:

for i in $(seq 1 4); do curl -s http://localhost/; echo; done

HAProxy sẽ bỏ app2 ra khỏi pool và chỉ xoay giữa app1app3. Không có request nào bị lỗi.

Bật lại:

docker compose start app2

Sau 2 lần health check thành công (khoảng 10 giây), HAProxy tự đưa app2 trở lại pool.

Các lỗi hay gặp

HAProxy khởi động xong nhưng request 503

Health check chạy ngay khi HAProxy start. Nếu backend chưa sẵn sàng, HAProxy đánh dấu chúng là DOWN và trả 503. Đợi 10–15 giây cho backend khởi xong rồi thử lại. Hoặc thêm healthcheck vào service backend trong Compose.

Lỗi cannot bind socket khi start

Port 80 hoặc 8404 đang bị chiếm bởi process khác. Kiểm tra:

sudo ss -tlnp | grep ':80\|:8404'

Config parse error

HAProxy validate config rất nghiêm. Xem log để biết dòng nào lỗi:

docker compose logs haproxy

Lỗi thường gặp: thiếu dấu cách trước check, sai tên server trong backend, hoặc timeout không đúng đơn vị.

Backend tên không resolve được

Trong Docker Compose, HAProxy dùng tên service (vd: app1) làm hostname. Nếu bạn đổi tên service thì phải đổi trong haproxy.cfg theo.

Chuyển sang leastconn khi nào

roundrobin phù hợp khi các request nhẹ và xử lý nhanh. Với bài toán như:

  • WebSocket
  • Upload file lớn
  • API xử lý lâu

Đổi sang leastconn để tránh một backend bị chất đống request trong khi backend kia rảnh:

backend web_backends
    balance leastconn
    ...

Session sticky với source

Nếu app cần user luôn vào cùng một backend (ví dụ: dùng local session, không có shared cache), dùng source:

backend web_backends
    balance source
    hash-type consistent
    ...

hash-type consistent giúp khi thêm hoặc bớt backend, chỉ một phần nhỏ request bị chuyển backend thay vì toàn bộ bị xáo trộn.

Khi nào không dùng HAProxy theo cách này

  • App đang trên Kubernetes: dùng Service + Ingress thay vì tự quản lý HAProxy
  • Cần SSL termination phức tạp với nhiều domain: Nginx hoặc Caddy dễ cấu hình hơn
  • Chỉ có một backend duy nhất: không cần load balancer, tốn tài nguyên vô ích

Checklist trước khi lên production

  • Đổi stats auth sang password mạnh hơn, hoặc tắt Stats UI nếu không cần
  • Bật SSL trên frontend nếu app expose ra Internet
  • Cấu hình log ra file hoặc log collector thay vì stdout
  • Điều chỉnh timeout phù hợp với đặc thù của app
  • Test health check bằng cách kill từng backend thủ công trước khi deploy thật