Chạy nhiều site trên 1 VPS với Docker và nginx-proxy

Bạn có một VPS, muốn chạy WordPress cho blog, Nextcloud cho lưu trữ, và một site React tĩnh khác, nhưng chỉ có một cổng 80 và 443. Cách cổ điển là cài Nginx lên host rồi cấu hình từng virtual host, nhưng nếu các site chạy trong container thì việc quản lý port và SSL dễ trở thành cơn ác mộng. nginx-proxy kết hợp với Docker giải quyết triệt để bài toán này: tự động dò container, cấp chứng chỉ SSL, và định tuyến request đúng nơi. Tất cả trên một VPS.
Yêu cầu trước khi bắt đầu
- Một VPS chạy Ubuntu 24.04 LTS hoặc Debian 12 (tối thiểu 2GB RAM, 1 vCPU). Có thể chọn VPS Linux có full root.
- Docker và Docker Compose (v2) đã cài đặt. Nếu chưa, tham khảo bài cách cài Docker trên VPS Ubuntu chi tiết từ A đến Z.
- Tên miền trỏ IP VPS (A record cho từng subdomain).
- User có quyền sudo (không dùng root).
- Firewall mở cổng 80, 443.
Vì sao nên dùng nginx-proxy thay vì cấu hình tay?
Khi bạn chạy nhiều container trên cùng một VPS, mỗi container cần ánh xạ riêng cổng HTTP. Ví dụ: WordPress ở cổng 8080, site React ở cổng 3000. Bạn phải nhớ port của từng cái và cấu hình Nginx thủ công để reverse proxy từng domain sang từng port. nginx-proxy tự động hóa việc này: nó lắng nghe sự kiện Docker, khi thấy container nào có biến môi trường VIRTUAL_HOST, nó tự tạo cấu hình proxy và cập nhật Nginx ngay lập tức, không cần reload thủ công. Kết hợp với acme-companion, SSL Let's Encrypt được cấp và gia hạn hoàn toàn tự động.
Bước 1 - Tạo Docker network dùng chung
Các container cần nói chuyện với nhau qua một mạng nội bộ. Tạo một network bridge tên web-proxy:
docker network create web-proxy
Mọi container muốn được proxy đều phải gắn vào network này.
Verify:
docker network ls | grep web-proxy
# Kết quả: web-proxy bridge local
Bước 2 - Khởi chạy nginx-proxy và acme-companion
Tạo thư mục làm việc:
mkdir -p ~/docker-proxy && cd ~/docker-proxy
Tạo file docker-compose.yml:
services:
nginx-proxy:
image: nginxproxy/nginx-proxy:latest
container_name: nginx-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx/html:/usr/share/nginx/html
- ./nginx/certs:/etc/nginx/certs
- ./nginx/vhost.d:/etc/nginx/vhost.d
- ./nginx/conf.d:/etc/nginx/conf.d
networks:
- web-proxy
acme-companion:
image: nginxproxy/acme-companion:latest
container_name: acme-companion
restart: unless-stopped
depends_on:
- nginx-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./nginx/certs:/etc/nginx/certs
- ./nginx/vhost.d:/etc/nginx/vhost.d
- ./nginx/html:/usr/share/nginx/html
- ./acme:/etc/acme.sh
environment:
- [email protected]
networks:
- web-proxy
networks:
web-proxy:
external: true
Giải thích:
nginx-proxy: container Nginx chính, nhận request từ cổng 80/443 và proxy đến container phù hợp.acme-companion: tự động xin và gia hạn SSL Let's Encrypt. Cần email hợp lệ ởDEFAULT_EMAIL.- Cả hai đều kết nối network
web-proxy(external: true vì đã tạo trước).
Khởi chạy:
docker compose up -d
Verify:
docker compose ps
# Cả hai container phải "Up" (running)
curl -I http://localhost
# Phải trả về HTTP/1.1 200 OK hoặc 503 (vì chưa có upstream), quan trọng là Nginx chạy
Bước 3 - Chạy site WordPress đầu tiên
Tạo thư mục riêng cho site:
mkdir -p ~/sites/wordpress && cd ~/sites/wordpress
File docker-compose.yml:
services:
db:
image: mariadb:11.4
restart: unless-stopped
volumes:
- ./db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root_secret
MYSQL_DATABASE: wordpress
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_secret
wordpress:
image: wordpress:6.7-php8.3-fpm
restart: unless-stopped
depends_on:
- db
volumes:
- ./wp_data:/var/www/html
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_secret
WORDPRESS_DB_NAME: wordpress
networks:
- web-proxy
nginx-wordpress:
image: nginx:1.26
restart: unless-stopped
depends_on:
- wordpress
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./wp_data:/var/www/html:ro
environment:
- VIRTUAL_HOST=blog.example.com
- LETSENCRYPT_HOST=blog.example.com
- [email protected]
networks:
- web-proxy
networks:
web-proxy:
external: true
Bạn cần một file nginx.conf tối thiểu để Nginx container này phục vụ file PHP qua FastCGI. Tạo file nginx.conf cùng thư mục:
server {
listen 80;
index index.php;
root /var/www/html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Quan trọng: VIRTUAL_HOST phải đúng tên miền bạn trỏ về VPS. LETSENCRYPT_HOST kích hoạt SSL tự động.
Khởi chạy:
docker compose up -d
Bước 4 - Thêm site thứ hai, ví dụ site tĩnh HTML
Tạo thư mục site tĩnh:
mkdir -p ~/sites/static-site && cd ~/sites/static-site
File docker-compose.yml:
services:
static:
image: nginx:1.26-alpine
restart: unless-stopped
volumes:
- ./html:/usr/share/nginx/html:ro
- ./default.conf:/etc/nginx/conf.d/default.conf:ro
environment:
- VIRTUAL_HOST=static.example.com
- LETSENCRYPT_HOST=static.example.com
- [email protected]
networks:
- web-proxy
networks:
web-proxy:
external: true
Tạo file html/index.html với nội dung hello world, và file default.conf đơn giản:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
}
Khởi chạy:
docker compose up -d
Bước 5 - Verify hoạt động
Kiểm tra DNS: Đảm bảo blog.example.com và static.example.com trỏ đúng IP VPS.
Kiểm tra SSL:
docker logs acme-companion
# Tìm dòng "Created certificate" hoặc "Certificate issued" cho từng domain
Kiểm tra truy cập:
curl -I https://blog.example.com
# HTTP/1.1 200 OK, có header "Server: nginx/1.26" (từ nginx-proxy)
Kiểm tra network:
docker network inspect web-proxy
# Các container: nginx-proxy, acme-companion, wordpress_wordpress_1, wordpress_nginx-wordpress_1, static_static_1 đều có trong danh sách
Xử lý lỗi thường gặp
1. Lỗi "502 Bad Gateway" khi truy cập site
Nguyên nhân: container upstream (WordPress, Nginx PHP) chưa kết nối network proxy. Kiểm tra:
docker network inspect web-proxy | grep -c "wordpress_wordpress_1"
# Phải > 0
Nếu chưa, chạy docker compose up -d lại từ thư mục site.
2. Lỗi "503 Service Temporarily Unavailable"
Thường do nginx-proxy chưa kịp cập nhật cấu hình. Chờ 10-20 giây. Có thể reload Nginx:
docker exec nginx-proxy nginx -s reload
3. SSL không tự động cấp
Kiểm tra log acme-companion:
docker logs acme-companion | grep error
Nguyên nhân phổ biến: LETSENCRYPT_HOST không khớp với DNS, hoặc cổng 80 chưa mở (acme-companion cần xác thực HTTP-01).
Câu hỏi thường gặp
nginx-proxy có hỗ trợ HTTP/2 hoặc HTTP/3 không?
Có. nginx-proxy mới nhất hỗ trợ HTTP/2 mặc định khi có SSL. Để bật HTTP/3, cần cấu hình thêm file custom.conf nhưng không phổ biến do nhiều proxy chặn UDP.
Tôi muốn thêm site mới, cần làm gì?
Chỉ cần tạo thư mục mới, viết docker-compose.yml với biến môi trường VIRTUAL_HOST và LETSENCRYPT_HOST, chạy docker compose up -d. Không cần chỉnh sửa nginx-proxy.
Làm sao để tắt SSL cho site test?
Bỏ biến LETSENCRYPT_HOST khỏi container. Cổng 80 vẫn chạy qua nginx-proxy.
Tôi dùng database thuê ngoài, có cần đặt trong docker không?
Không. Bạn có thể kết nối container WordPress đến host hoặc remote database qua môi trường WORDPRESS_DB_HOST. Chỉ cần chắc chắn container có thể kết nối tới địa chỉ đó (qua các network thích hợp).
Mô hình này có hao RAM hơn so với cài Nginx trên host không?
Có một chút. nginx-proxy + acme-companion chiếm khoảng 100-150 MB RAM. Mỗi site thêm container Nginx phụ (10-20 MB). So với lợi ích về tự động hóa, con số này rất nhỏ.
Bài viết liên quan
- Cài đặt và tối ưu Nginx làm reverse proxy trên Ubuntu 24.04
- Cách cài Docker trên VPS Ubuntu chi tiết từ A đến Z
- Cấu hình Nginx reverse proxy và SSL Let's Encrypt cho n8n
- Docker networking: bridge, host, overlay khác nhau thế nào


