Trong bài viết này, mình sẽ hướng dẫn từng bước cách thiết lập HTTPS với Let’s Encrypt cho Nginx chạy trên Docker, theo hướng thực tế – dễ vận hành – dùng được cho production.
Nội dung bao gồm:
・Thiết lập Nginx chạy HTTPS trên Docker
・Cấp SSL/TLS certificate bằng Certbot
・Tự động gia hạn certificate
・Redirect HTTP → HTTPS
・Áp dụng các security headers cơ bản
Yêu cầu
・Docker đã cài đặt
・Docker Compose đã cài đặt
・Quyền quản trị hệ thống
・Domain đã trỏ về IP server
・Port 80 và 443 mở và trỏ về server
Thiết lập Nginx Webserver với Let’s Encrypt trên Docker
Quy trình thiết lập gồm các bước sau:・Tạo Docker Compose file
・Cấu hình Nginx server
・Tạo script setup SSL
・Chạy Certbot client
・Tự động gia hạn certificate
Bước 1: Tạo cấu trúc thư mục và .env
Tạo các thư mục cần thiết trong dự án:
mkdir -p docker/nginx/etc/nginx/templates
mkdir -p docker/nginx/etc/nginx/conf
mkdir -p docker/nginx/certs
Cấu trúc thư mục:
docker/
└── nginx/
├── setup-ssl.sh
├── entrypoint.sh
├── etc/nginx/
│ ├── nginx.conf
│ ├── conf/
│ └── templates/
│ ├── default.conf.template
│ ├── https.conf.template
│ └── upstream.conf.template
└── certs/ # Thư mục chứa certificates (gitignored)
File Structure
・entrypoint.sh: Script tự động generate nginx configs từ templates
・setup-ssl.sh: Script tạo SSL certificate trên host (chạy ngoài container)
・templates/: Nginx config templates
・https.conf.template: HTTPS config cho domain
・upstream.conf.template: PHP-FPM upstream config
・certs/: Thư mục chứa Let’s Encrypt certificates trên host (gitignored, mounted vào container)
Environment Variables
| Biến | Mô tả | Default |
|---|---|---|
DOCKER_NGINX_EXTERNAL_PORT | HTTP external port | 8000 |
DOCKER_NGINX_HTTPS_PORT | HTTPS external port | 8443 |
HTTPS_DOMAIN | Domain dùng HTTPS | example.com |
ENABLE_HTTPS | Bật HTTPS | true |
Bước 2: Tạo Dockerfile cho Nginx
Tạo file docker/Dockerfile với multi-stage build:
FROM nginx:1.28.0-alpine as nginx
WORKDIR /var/www/html
COPY docker/nginx/ /
COPY docker/nginx/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 80 443
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
Bước 3: Tạo Docker Compose file
Tạo file docker-compose.yml để chạy Nginx với HTTPS:
services:
nginx:
build:
context: .
dockerfile: docker/Dockerfile
target: nginx
environment:
ENABLE_BASIC_AUTH: "${ENABLE_BASIC_AUTH-false}"
PHP_FPM_HOST: app
PHP_FPM_PORT: 9000
ENABLE_HTTP_LOCALHOST: "false"
ENABLE_HTTPS: "true"
HTTPS_DOMAIN: "${HTTPS_DOMAIN-example.com}"
ports:
- "${DOCKER_NGINX_EXTERNAL_PORT-8000}:80"
- "${DOCKER_NGINX_HTTPS_PORT-8443}:443"
depends_on:
- app
- certbot
volumes:
- ".:/var/www/html"
- "./docker/nginx/certs:/etc/letsencrypt"
certbot:
image: certbot/certbot:latest
volumes:
- "./docker/nginx/certs:/etc/letsencrypt"
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
app:
build:
context: .
dockerfile: docker/Dockerfile
target: app
volumes:
- ".:/var/www/html"
environment:
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 1
command: ["development"]
Giải thích:webserver (nginx): container Nginx với HTTPScertbot: container tự động gia hạn certificateapp: container Laravel application
Volume ./docker/nginx/certs:/etc/letsencrypt: chia sẻ certificates giữa containers
Bước 4: Tạo file cấu hình Nginx
4.1. Tạo template HTTP (cho Let’s Encrypt validation)
Tạo file docker/nginx/etc/nginx/templates/default.conf.template:
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
client_max_body_size 40m;
access_log /dev/stdout;
error_log /dev/stderr;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
proxy_hide_header X-Powered-By;
charset utf-8;
location = /health {
return 200 'OK!';
add_header Content-Type text/plain;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location / {
auth_basic $basic_auth;
auth_basic_user_file /etc/nginx/.htpasswd;
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
set $path_info $fastcgi_path_info;
fastcgi_index index.php;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_pass php-fpm;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Lưu ý: Block location ~ /\.(?!well-known).* chặn các file ẩn nhưng cho phép .well-known để Let’s Encrypt validate.
4.2. Tạo template HTTPS
Tạo file docker/nginx/etc/nginx/templates/https.conf.template:
# HTTPS server for ${HTTPS_DOMAIN}
server {
listen 443 ssl;
http2 on;
server_name ${HTTPS_DOMAIN};
root /var/www/html/public;
index index.php;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/${HTTPS_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${HTTPS_DOMAIN}/privkey.pem;
client_max_body_size 40m;
access_log /dev/stdout;
error_log /dev/stderr;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
proxy_hide_header X-Powered-By;
charset utf-8;
location = /health {
return 200 'OK!';
add_header Content-Type text/plain;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location / {
auth_basic $basic_auth;
auth_basic_user_file /etc/nginx/.htpasswd;
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
set $path_info $fastcgi_path_info;
fastcgi_index index.php;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_pass php-fpm;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
# Redirect HTTP to HTTPS for ${HTTPS_DOMAIN}
server {
listen 80;
server_name ${HTTPS_DOMAIN};
location / {
return 301 https://$host$request_uri;
}
}
Giải thích:
Server block đầu: HTTPS trên port 443 với HTTP/2
Server block thứ hai: redirect HTTP → HTTPS
Security headers: HSTS, X-Frame-Options, X-Content-Type-Options
Bước 5: Tạo Entrypoint script
Tạo file docker/nginx/entrypoint.sh để tự động generate config từ template:
#!/bin/sh
set -e
# Set BASIC_AUTH_VALUE based on ENABLE_BASIC_AUTH
if [ "$ENABLE_BASIC_AUTH" = "true" ]; then
export BASIC_AUTH_VALUE="Administrator's Area"
else
export BASIC_AUTH_VALUE="off"
fi
# Generate map config from template
envsubst '$$BASIC_AUTH_VALUE' < /etc/nginx/templates/map.conf.template > /etc/nginx/conf.d/map.conf
# Generate upstream config
envsubst '$$PHP_FPM_HOST $$PHP_FPM_PORT' < /etc/nginx/templates/upstream.conf.template > /etc/nginx/conf.d/upstream.conf
# Generate nginx configs from templates
if [ "$ENABLE_HTTP_LOCALHOST" = "true" ]; then
envsubst '$$PHP_FPM_HOST $$PHP_FPM_PORT' < /etc/nginx/templates/localhost.conf.template > /etc/nginx/conf.d/server-localhost.conf
fi
# Handle HTTPS setup
if [ "$ENABLE_HTTPS" = "true" ] && [ -n "$HTTPS_DOMAIN" ]; then
# Create directories if they don't exist
mkdir -p /etc/letsencrypt/live/${HTTPS_DOMAIN}
if [ -f "/etc/letsencrypt/live/${HTTPS_DOMAIN}/fullchain.pem" ]; then
envsubst '$$PHP_FPM_HOST $$PHP_FPM_PORT $$HTTPS_DOMAIN' < /etc/nginx/templates/https.conf.template > /etc/nginx/conf.d/server-https.conf
fi
fi
# Execute the main command
exec "$@"
Script này:
・Tạo config từ template dựa trên biến môi trường
・Chỉ tạo HTTPS config nếu certificate tồn tại
・Dùng envsubst để thay thế biến
Bước 6: Tạo script setup SSL
Tạo file docker/nginx/setup-ssl.sh để tạo certificate trên host:
#!/bin/bash
set -e
# Get script directory and project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# Configuration
DOMAIN="${HTTPS_DOMAIN:-example.com}"
CERT_DIR="${PROJECT_ROOT}/docker/nginx/certs"
echo "Setting up SSL certificate for ${DOMAIN} on host..."
echo "Certificate will be saved to: ${CERT_DIR}"
# Create directories on host
mkdir -p "${CERT_DIR}"
# Check if certificate already exists
if [ -f "${CERT_DIR}/live/${DOMAIN}/fullchain.pem" ]; then
echo "Certificate already exists for ${DOMAIN}"
echo "Location: ${CERT_DIR}/live/${DOMAIN}/"
exit 0
fi
cd "${PROJECT_ROOT}"
# Request certificate using docker (certificate saved on host, mounted into container)
echo "Requesting certificate (port 80 must be available)..."
docker run --rm \
-v "${CERT_DIR}:/etc/letsencrypt" \
-p 80:80 \
certbot/certbot certonly \
--standalone \
--agree-tos \
-d "${DOMAIN}"
echo ""
echo "Certificate created successfully on host!"
echo "Location: ${CERT_DIR}/live/${DOMAIN}/"
echo "Files: fullchain.pem, privkey.pem"
echo ""
echo "Certificate will be automatically mounted into nginx container."
echo "You can now run: docker-compose up -d"
Lưu ý: Script này chạy trên host, không trong container. Certificate được lưu trên host và mount vào container.
Bước 7: Chạy Certbot để lấy certificate
7.1. Test với dry-run (khuyến nghị)
Trước khi lấy certificate thật, test với --dry-run:
chmod +x docker/nginx/setup-ssl.sh
# Test với dry-run
docker run --rm \
-v "$(pwd)/docker/nginx/certs:/etc/letsencrypt" \
-p 80:80 \
certbot/certbot certonly \
--standalone \
--dry-run \
--agree-tos \
-d your-domain.com
Lưu ý: Thay your-domain.com bằng domain của bạn.
7.2. Lấy certificate thật
Nếu dry-run thành công, chạy script để lấy certificate:
HTTPS_DOMAIN=your-domain.com ./docker/nginx/setup-ssl.sh
Hoặc dùng domain mặc định:
./docker/nginx/setup-ssl.sh
Quá trình:
- Script kiểm tra certificate đã tồn tại chưa
- Nếu chưa, chạy Certbot với –standalone mode
- Certbot bind port 80 để Let’s Encrypt validate
- Certificate được lưu tại ./docker/nginx/certs/live/your-domain.com/
Lưu ý:
・Đảm bảo port 80 mở và trỏ về server.
・Nếu Nginx đang chạy trên port 80, tạm dừng trước khi chạy script.
Bước 8: Khởi động Docker Compose
Sau khi có certificate, khởi động services:
docker-compose up -d
Kiểm tra containers đang chạy:
docker-compose ps
Kiểm tra logs:
docker-compose logs nginx
docker-compose logs certbot
Bước 9: Kiểm tra HTTPS
Truy cập domain qua HTTPS:
curl -I https://your-domain.com:8443
Hoặc mở trình duyệt và truy cập: https://your-domain.com:8443
Bạn sẽ thấy:
・Certificate hợp lệ (không có cảnh báo)
・Tự động redirect HTTP → HTTPS
・Security headers được áp dụng
Bước 10: Gia hạn certificates
Let’s Encrypt certificates có thời hạn 90 ngày. Có 2 cách gia hạn:
10.1. Tự động gia hạn (đã cấu hình)
Service certbot trong docker-compose.yml tự động gia hạn mỗi 12 giờ:
certbot:
image: certbot/certbot:latest
volumes:
- "./docker/nginx/certs:/etc/letsencrypt"
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
Certbot chỉ gia hạn khi certificate sắp hết hạn (trong vòng 30 ngày).
10.2. Gia hạn thủ công
Nếu cần gia hạn ngay:
docker run --rm \
-v "$(pwd)/docker/nginx/certs:/etc/letsencrypt" \
certbot/certbot renew
Hoặc trong container:
docker-compose exec certbot certbot renew
Sau khi gia hạn, reload Nginx để áp dụng certificate mới:
docker-compose exec nginx nginx -s reload
Hoặc restart container:
docker-compose restart nginx
Troubleshooting
Lỗi: “Port 80 is already in use”
Nguyên nhân: Port 80 đang được sử dụng bởi service khác.
Giải pháp:
# Kiểm tra process đang dùng port 80
sudo lsof -i :80
# Tạm dừng Nginx nếu đang chạy
docker-compose down
# Sau đó chạy lại setup-ssl.sh
./docker/nginx/setup-ssl.sh
Lỗi: “Failed to obtain certificate”
Nguyên nhân:
・Domain chưa trỏ về IP server
・Port 80 bị firewall chặn
・Let’s Encrypt không thể truy cập domain
Giải pháp:
# Kiểm tra DNS
nslookup your-domain.com
# Kiểm tra port 80
curl -I http://your-domain.com
# Kiểm tra firewall
sudo ufw status
Lỗi: “Nginx không load HTTPS config”
Nguyên nhân:
・Certificate chưa tồn tại
・Biến môi trường chưa đúng
・Volume mount sai
Giải pháp:
# Kiểm tra certificate
ls -la docker/nginx/certs/live/your-domain.com/
# Kiểm tra biến môi trường
docker-compose config
# Kiểm tra volume mount trong container
docker-compose exec nginx ls -la /etc/letsencrypt/live/
Tổng kết
Sau khi hoàn thành các bước trên, bạn đã:
・Thiết lập Nginx với HTTPS trên Docker
・Cấu hình Let’s Encrypt SSL/TLS certificates
・Tự động gia hạn certificates
・Redirect HTTP → HTTPS
・Áp dụng security headers