Hướng dẫn từng bước thiết lập Let’s Encrypt SSL/TLS cho Nginx trên Docker.

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ếnMô tảDefault
DOCKER_NGINX_EXTERNAL_PORTHTTP external port8000
DOCKER_NGINX_HTTPS_PORTHTTPS external port8443
HTTPS_DOMAINDomain dùng HTTPSexample.com
ENABLE_HTTPSBật HTTPStrue

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 HTTPS
certbot: container tự động gia hạn certificate
app: 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:

  1. Script kiểm tra certificate đã tồn tại chưa
  2. Nếu chưa, chạy Certbot với –standalone mode
  3. Certbot bind port 80 để Let’s Encrypt validate
  4. 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

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like