Trong thế giới kỹ thuật số ngày nay, WordPress vẫn là một “ông lớn” trong lĩnh vực hệ thống quản lý nội dung (CMS), được hàng triệu website tin dùng từ các blog cá nhân đến những trang thương mại điện tử phức tạp. Tuy nhiên, việc cài đặt và quản lý WordPress theo cách truyền thống thường tiêu tốn nhiều thời gian và công sức, đặc biệt khi bạn phải tự tay thiết lập toàn bộ môi trường như một LAMP (Linux, Apache, MySQL, PHP) hay LEMP (Linux, Nginx, MySQL, PHP) stack.

May mắn thay với sự ra đời của Docker và Docker Compose, chúng ta có một giải pháp mạnh mẽ để đơn giản hóa quá trình này. Docker cho phép chúng ta đóng gói ứng dụng và tất cả các phụ thuộc vào các “image” tiêu chuẩn, sau đó chạy chúng trong các “container” biệt lập. Docker Compose còn đi xa hơn, giúp chúng ta điều phối nhiều container cùng lúc, ví dụ như ứng dụng WordPress và cơ sở dữ liệu của nó, để chúng có thể giao tiếp một cách liền mạch.
Tôi sẽ chia sẻ với bạn cách chúng ta có thể xây dựng một hệ thống WordPress đa container, sử dụng Docker Compose. Thiết lập này bao gồm một cơ sở dữ liệu MySQL, một máy chủ web Nginx, và chính ứng dụng WordPress. Không chỉ vậy, chúng ta còn đảm bảo an toàn cho website bằng cách tích hợp chứng chỉ TLS/SSL miễn phí từ Let’s Encrypt và thiết lập một cronjob để tự động gia hạn chứng chỉ, giúp trang web của bạn luôn được bảo vệ.
Chuẩn bị
Nếu bạn đang sử dụng Ubuntu phiên bản 16.04 trở xuống, chúng tôi khuyên bạn nên nâng cấp lên phiên bản mới nhất vì Ubuntu không còn hỗ trợ các phiên bản này nữa.
- Một máy chủ chạy Ubuntu, cùng với một người dùng không phải root có quyền sudo và tường lửa đang hoạt động
- Cài đặt docker
- Cài đặt Docker Compose
- Đăng ký 1 domain
- Cấu hình 2 bản ghị DNS sau
- Bản ghi A cho your_domain trỏ đến địa chỉ IP công khai của máy chủ của bạn.
- Bản ghi A cho www.your_domain trỏ đến địa chỉ IP công khai của máy chủ của bạn.
Các bước cài đặt
Bước 1: Định nghĩa cấu hình cho Web Server Nginx
Bước 2: Định nghĩa các biến môi trường
Bước 3: Định nghĩa các dịch vụ với Docker Compose
Bước 4: Lấy chứng chỉ SSL và thông tin xác thực
Bước 5: Sửa đổi cấu hình Web Server và định nghĩa dịch vụ
Bước 6: Hoàn tất cài đặt thông qua giao diện Web
Bước 7: Tự động gia hạn chứng chỉ SSL
Bước 1: Định nghĩa cấu hình cho Web Server Nginx
Trước khi chúng ta khởi động bất kỳ container nào, việc đầu tiên là định nghĩa cấu hình cho máy chủ web Nginx của chúng ta. File cấu hình này sẽ chứa các block location dành riêng cho WordPress và một block location đặc biệt để chuyển hướng các yêu cầu xác minh của Let’s Encrypt đến client Certbot để gia hạn chứng chỉ tự động.
Đầu tiên, chúng ta tạo một thư mục dự án cho thiết lập WordPress. Tôi gọi nó là wordpress, bạn có thể đặt tên khác tùy ý:
mkdir wordpress
Sau đó, chúng ta di chuyển vào thư mục này:
cd wordpress
Tiếp theo, tạo một thư mục để chứa file cấu hình Nginx:
mkdir nginx-conf
Mở file cấu hình Nginx bằng nano hoặc trình soạn thảo yêu thích của bạn:
nano nginx-conf/nginx.conf
Trong file này, bạn sẽ thêm một server block với các chỉ thị cho server_name và document root, cùng với các location block để xử lý yêu cầu của Certbot, PHP, và các tài nguyên tĩnh. Hãy nhớ thay thế your_domain bằng tên miền của bạn:
server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
index index.php index.html index.htm;
root /var/www/html;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \\\\.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\\\\.php)(/.+)$;
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\\\\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off; access_log off;
}
location = /robots.txt {
log_not_found off; access_log off; allow all;
}
location ~* \\\\.(css|gif|ico|jpeg|jpg|js|png)$ {
expires max;
log_not_found off;
}
}
Khối server block của chúng ta bao gồm các thông tin sau:
Các directive:
- listen: Chỉ định cho Nginx lắng nghe trên cổng 80, cho phép bạn sử dụng plugin webroot của Certbot để yêu cầu chứng chỉ. Lưu ý rằng ở đây bạn chưa cấu hình cổng 443 bạn sẽ cập nhật cấu hình để bổ sung SSL sau khi đã lấy chứng chỉ thành công.
- server_name: Xác định tên máy chủ và server block sẽ được dùng cho các request đến server của bạn. Hãy chắc chắn thay your_domain bằng domain thật của bạn.
- index: Xác định các file sẽ được dùng làm index khi xử lý request. Ở đây, bạn đã chỉnh lại thứ tự mặc định, đưa index.php lên trước index.html, để Nginx ưu tiên xử lý file index.php khi có thể.
- root: Xác định thư mục gốc cho các request đến server. Thư mục này, /var/www/html, được tạo ra tại thời điểm build như một mount point theo hướng dẫn trong Dockerfile của WordPress. Các hướng dẫn này cũng đảm bảo rằng các file từ bản phát hành WordPress sẽ được mount vào volume này.
Các Location Block:
- location ~ /.well-known/acme-challenge: Xử lý các request đến thư mục .well-known, nơi Certbot sẽ đặt file tạm để xác thực rằng DNS domain của bạn trỏ về server. Với cấu hình này, bạn có thể dùng plugin webroot của Certbot để lấy chứng chỉ cho domain.
- location /: Sử dụng directive try_files để kiểm tra file khớp với từng URI request. Thay vì trả về 404 Not Found theo mặc định, request sẽ được chuyển tiếp đến file index.php của WordPress cùng với tham số request.
- location ~ .php$: Xử lý các request PHP và proxy đến container wordpress. Vì WordPress Docker image được xây dựng dựa trên php:fpm, bạn sẽ thêm các cấu hình dành riêng cho giao thức FastCGI. Nginx yêu cầu một PHP processor độc lập để xử lý PHP request, và ở đây sẽ là php-fpm tích hợp trong image php:fpm. Block này còn bao gồm các directive, biến và tùy chọn dành riêng cho FastCGI để proxy request đến WordPress trong container, thiết lập index ưu tiên cho URI request, và phân tích các URI request.
- location ~ /.ht: Xử lý các file .htaccess vì Nginx sẽ không phục vụ chúng. Directive deny_all đảm bảo các file .htaccess sẽ không bao giờ được gửi cho người dùng.
- location = /favicon.ico, location = /robots.txt: Đảm bảo các request đến /favicon.ico và /robots.txt sẽ không bị ghi log.
- location ~ .(css|gif|ico|jpeg|jpg|js|png)$**: Tắt log cho các request asset tĩnh và đảm bảo rằng chúng có thể được cache lâu dài, vì những asset này thường tốn chi phí khi phục vụ.
Sau khi chỉnh sửa xong, hãy lưu và thoát file. Nếu dùng nano, nhấn CTRL+X, sau đó Y, rồi ENTER.
Khi cấu hình Nginx đã hoàn tất, bạn có thể chuyển sang bước tạo biến môi trường (environment variables) để truyền cho các container ứng dụng và database khi runtime.
Bước 2: Định nghĩa các biến môi trường
Các container cơ sở dữ liệu và ứng dụng WordPress của chúng ta cần truy cập vào một số biến môi trường để dữ liệu ứng dụng được lưu trữ và truy cập đúng cách. Các biến này bao gồm thông tin nhạy cảm (mật khẩu root MySQL, tên và mật khẩu người dùng cơ sở dữ liệu ứng dụng) và thông tin không nhạy cảm (tên và host cơ sở dữ liệu ứng dụng).
Thay vì đặt tất cả các giá trị này trực tiếp trong file docker-compose.yml, chúng ta sẽ đặt các giá trị nhạy cảm vào một file .env và hạn chế việc chia sẻ nó. Điều này giúp ngăn chặn các giá trị này bị sao chép vào các kho lưu trữ dự án của bạn và bị lộ công khai.
Trong thư mục dự án chính của bạn, ~/wordpress, hãy mở một file có tên .env:
nano .env
Bạn thêm các tên biến và giá trị sau vào file. Hãy nhớ cung cấp các giá trị của riêng bạn cho mỗi biến:
MYSQL_ROOT_PASSWORD=your_root_password
MYSQL_USER=your_wordpress_database_user
MYSQL_PASSWORD=your_wordpress_database_password
Lưu và đóng file khi bạn hoàn tất chỉnh sửa.
Vì file .env chứa thông tin nhạy cảm, chúng ta cần đảm bảo rằng nó được đưa vào file .gitignore và .dockerignore của dự án. Điều này sẽ báo cho Git và Docker biết những file nào không được sao chép vào kho lưu trữ Git và Docker image tương ứng.
Nếu bạn có kế hoạch làm việc với Git, hãy khởi tạo thư mục làm việc hiện tại của bạn thành một repository:
git init
Sau đó tạo và mở file .gitignore:
nano .gitignore
Thêm .env vào file:
.env
Lưu và đóng file. Tương tự, thêm .env vào file .dockerignore:
nano .dockerignore
Thêm .env vào file:
.env
Bạn cũng có thể tùy chọn thêm các file và thư mục liên quan đến quá trình phát triển ứng dụng của bạn dưới đây:
.env
.git
docker-compose.yml
.dockerignore
Lưu và đóng file khi bạn hoàn tất.
Với các thông tin nhạy cảm đã được bảo mật, giờ là lúc định nghĩa các dịch vụ chính trong file docker-compose.yml của chúng ta.
Bước 3: Định nghĩa các dịch vụ với Docker Compose
File docker-compose.yml của chúng ta sẽ chứa các định nghĩa dịch vụ cho toàn bộ thiết lập. Một “service” trong Compose là một container đang chạy, và định nghĩa dịch vụ sẽ chỉ rõ cách mỗi container sẽ hoạt động.
Chúng ta sẽ tạo các container khác nhau cho cơ sở dữ liệu, ứng dụng WordPress, và máy chủ web Nginx. Ngoài ra, chúng ta còn tạo một container để chạy client Certbot nhằm lấy chứng chỉ SSL cho máy chủ web.
Để bắt đầu, hãy tạo và mở file docker-compose.yml:
nano docker-compose.yml
Thêm đoạn mã sau để định nghĩa phiên bản Compose file và dịch vụ cơ sở dữ liệu db:
version: '3'
services:
db:
image: mysql:8.0
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MYSQL_DATABASE=wordpress
volumes:
- dbdata:/var/lib/mysql
command: '--default-authentication-plugin=mysql_native_password'
networks:
- app-network
Giải thích dịch vụ db:
- image: Chỉ định cho Compose biết cần pull image nào để tạo container. Ở đây bạn đang pin image
mysql:8.0để tránh xung đột trong tương lai, do imagemysql:latestsẽ liên tục được cập nhật. - container_name: Chỉ định tên cho container.
- restart: Định nghĩa chính sách khởi động lại của container. Giá trị mặc định là
no, nhưng ở đây bạn đã cấu hình để container tự động khởi động lại trừ khi được dừng thủ công. - env_file: Tùy chọn này cho Compose biết rằng bạn muốn thêm các biến môi trường từ một file tên
.env, nằm trong build context. Trong trường hợp này, build context chính là thư mục hiện tại của bạn. - environment: Tùy chọn này cho phép bạn thêm các biến môi trường bổ sung, ngoài những biến đã được định nghĩa trong file
.env. Ở đây bạn sẽ thiết lập biếnMYSQL_DATABASE=wordpressđể đặt tên cho cơ sở dữ liệu ứng dụng. Vì đây không phải thông tin nhạy cảm, bạn có thể khai báo trực tiếp trong filedocker-compose.yml. - volumes: Tại đây, bạn gắn (mount) một volume có tên dbdata vào thư mục
/var/lib/mysqltrên container. Đây là thư mục dữ liệu tiêu chuẩn của MySQL trong hầu hết các bản phân phối. - command: Tùy chọn này cho phép bạn ghi đè lệnh CMD mặc định của image. Trong trường hợp này, bạn thêm một tùy chọn vào lệnh
mysqldtiêu chuẩn của Docker image (lệnh khởi động MySQL server trong container). Tùy chọn--default-authentication-plugin=mysql_native_passwordsẽ thiết lập biến hệ thống--default-authentication-pluginthànhmysql_native_password, chỉ định cơ chế xác thực sẽ được sử dụng cho các yêu cầu xác thực mới. Do PHP (và theo đó là image WordPress) không hỗ trợ cơ chế xác thực mặc định mới hơn của MySQL, bạn cần chỉnh cấu hình này để người dùng cơ sở dữ liệu ứng dụng có thể xác thực. - networks: Chỉ định rằng dịch vụ ứng dụng của bạn sẽ tham gia vào mạng app-network, được định nghĩa ở cuối file.
Tiếp theo, bên dưới định nghĩa dịch vụ db, thêm định nghĩa cho dịch vụ ứng dụng wordpress của bạn:
...
wordpress:
depends_on:
- db
image: wordpress:5.1.1-fpm-alpine
container_name: wordpress
restart: unless-stopped
env_file: .env
environment:
- WORDPRESS_DB_HOST=db:3306
- WORDPRESS_DB_USER=$MYSQL_USER
- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD
- WORDPRESS_DB_NAME=wordpress
volumes:
- wordpress:/var/www/html
networks:
- app-network
Trong định nghĩa service này, bạn đang đặt tên cho container và xác định chính sách khởi động lại, tương tự như với service db. Ngoài ra, bạn cũng thêm một số tùy chọn riêng cho container này:
- depends_on: Tùy chọn này đảm bảo các container sẽ khởi động theo đúng thứ tự phụ thuộc, với container
wordpresschỉ khởi động sau khi containerdbđã chạy. Ứng dụng WordPress của bạn phụ thuộc vào sự tồn tại của cơ sở dữ liệu và người dùng ứng dụng, do đó việc thể hiện mối quan hệ phụ thuộc này sẽ giúp ứng dụng khởi động đúng cách. - image: Trong cấu hình này, bạn sử dụng image WordPress 5.1.1-fpm-alpine. Như đã đề cập ở Bước 1, image này đảm bảo ứng dụng của bạn sẽ có sẵn php-fpm processor mà Nginx yêu cầu để xử lý PHP. Đây cũng là một alpine image (dựa trên Alpine Linux), giúp giữ kích thước image tổng thể nhỏ gọn.
- env_file: Tương tự, bạn chỉ định rằng các giá trị sẽ được lấy từ file
.env, nơi bạn đã định nghĩa người dùng và mật khẩu cơ sở dữ liệu ứng dụng. - environment: Ở đây, bạn sử dụng các giá trị trong file
.env, nhưng gán chúng cho các biến môi trường mà WordPress image mong đợi:WORDPRESS_DB_USERWORDPRESS_DB_PASSWORD
Bạn cũng định nghĩa biến
WORDPRESS_DB_HOST, trỏ đến MySQL server chạy trong containerdb, truy cập được qua cổng mặc định của MySQL là 3306. BiếnWORDPRESS_DB_NAMEsẽ trùng với giá trị bạn đã khai báo trong service MySQL cho biếnMYSQL_DATABASE: wordpress. - volumes: Bạn mount một volume có tên wordpress vào mountpoint
/var/www/htmldo WordPress image tạo ra. Việc dùng named volume theo cách này cho phép bạn chia sẻ mã nguồn ứng dụng với các container khác. - networks: Bạn cũng thêm container
wordpressvào mạng app-network.
Tiếp theo, bên dưới định nghĩa dịch vụ wordpress, thêm định nghĩa cho dịch vụ máy chủ web webserver (Nginx):
...
webserver:
depends_on:
- wordpress
image: nginx:1.15.12-alpine
container_name: webserver
restart: unless-stopped
ports:
- "80:80"
volumes:
- wordpress:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- app-network
Ở đây, bạn đặt tên cho container và cấu hình nó phụ thuộc vào container wordpress trong thứ tự khởi động. Bạn cũng sử dụng một alpine image, cụ thể là image Nginx 1.15.12-alpine.
Định nghĩa service này cũng bao gồm các tùy chọn sau:
- ports: Mở cổng 80 để kích hoạt các tùy chọn cấu hình mà bạn đã định nghĩa trong file
nginx.confở Bước 1. - volumes: Bạn định nghĩa kết hợp giữa named volumes và bind mounts:
wordpress:/var/www/html: Mount mã nguồn ứng dụng WordPress vào thư mục/var/www/html(thư mục bạn đã thiết lập làm root trong khối cấu hình server của Nginx)../nginx-conf:/etc/nginx/conf.d: Bind mount thư mục cấu hình Nginx trên host vào thư mục tương ứng trong container, đảm bảo mọi thay đổi bạn thực hiện trên host sẽ được phản ánh ngay trong container.certbot-etc:/etc/letsencrypt: Mount chứng chỉ và khóa của Let’s Encrypt cho domain của bạn vào thư mục thích hợp trong container.
- networks: Bạn cũng thêm container này vào mạng app-network.
Cuối cùng, bên dưới định nghĩa dịch vụ webserver, thêm định nghĩa dịch vụ certbot. Hãy đảm bảo thay thế địa chỉ email và tên miền bằng thông tin của riêng bạn:
...
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- wordpress:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --staging -d your_domain -d www.your_domain
Định nghĩa này yêu cầu Compose pull image certbot/certbot từ Docker Hub. Nó cũng sử dụng named volumes để chia sẻ tài nguyên với container Nginx, bao gồm chứng chỉ và khóa domain trong certbot-etc cũng như mã nguồn ứng dụng trong wordpress.
Một lần nữa, bạn dùng depends_on để chỉ định rằng container certbot sẽ chỉ khởi động sau khi service webserver đã chạy.
Bạn cũng thêm tùy chọn command, chỉ định một subcommand chạy cùng với lệnh mặc định certbot của container. Subcommand certonly sẽ lấy chứng chỉ với các tùy chọn sau:
- -webroot: Chỉ định cho Certbot dùng plugin webroot để đặt file vào thư mục webroot phục vụ xác thực. Plugin này dựa trên phương thức xác thực HTTP-01, sử dụng HTTP request để chứng minh rằng Certbot có thể truy cập tài nguyên từ một server phản hồi theo tên miền đã cho.
- -webroot-path: Xác định đường dẫn tới thư mục webroot.
- -email: Địa chỉ email bạn muốn dùng cho đăng ký và khôi phục.
- -agree-tos: Xác nhận rằng bạn đồng ý với ACME’s Subscriber Agreement.
- -no-eff-email: Chỉ định rằng bạn không muốn chia sẻ email của mình với Electronic Frontier Foundation (EFF). (Bạn có thể bỏ tùy chọn này nếu muốn chia sẻ).
- -staging: Yêu cầu Certbot sử dụng môi trường staging của Let’s Encrypt để lấy chứng chỉ thử nghiệm. Tùy chọn này giúp bạn kiểm thử cấu hình và tránh vượt quá giới hạn số lần request domain.
- d: Cho phép bạn chỉ định các domain muốn áp dụng cho request. Trong ví dụ này, bạn khai báo your_domain và www.your_domain. Hãy thay thế chúng bằng domain thực tế của bạn.
Bên dưới định nghĩa dịch vụ certbot, thêm định nghĩa cho volumes và networks:
...
volumes:
certbot-etc:
wordpress:
dbdata:
networks:
app-network:
driver: bridge
Khóa volumes ở cấp cao nhất định nghĩa các volume: certbot-etc, wordpress, và dbdata. Khi Docker tạo volume, nội dung của volume sẽ được lưu trong một thư mục trên filesystem của host tại /var/lib/docker/volumes/, được Docker quản lý. Nội dung của mỗi volume sau đó sẽ được mount từ thư mục này vào bất kỳ container nào sử dụng volume đó. Cách làm này cho phép chia sẻ mã nguồn và dữ liệu giữa các container.
Mạng bridge do người dùng định nghĩa app-network cho phép các container giao tiếp với nhau vì chúng cùng chạy trên một Docker daemon host. Điều này giúp tối ưu hóa lưu lượng và giao tiếp trong ứng dụng, vì nó mở tất cả các cổng giữa các container trong cùng một bridge network mà không cần phải expose bất kỳ cổng nào ra bên ngoài. Nhờ đó, các container db, wordpress, và webserver có thể giao tiếp với nhau, và bạn chỉ cần expose cổng 80 để người dùng truy cập giao diện ứng dụng.
Đây là toàn bộ file docker-compose.yml sau khi hoàn tất:
version: '3'
services:
db:
image: mysql:8.0
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MYSQL_DATABASE=wordpress
volumes:
- dbdata:/var/lib/mysql
command: '--default-authentication-plugin=mysql_native_password'
networks:
- app-network
wordpress:
depends_on:
- db
image: wordpress:5.1.1-fpm-alpine
container_name: wordpress
restart: unless-stopped
env_file: .env
environment:
- WORDPRESS_DB_HOST=db:3306
- WORDPRESS_DB_USER=$MYSQL_USER
- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD
- WORDPRESS_DB_NAME=wordpress
volumes:
- wordpress:/var/www/html
networks:
- app-network
webserver:
depends_on:
- wordpress
image: nginx:1.15.12-alpine
container_name: webserver
restart: unless-stopped
ports:
- "80:80"
volumes:
- wordpress:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- app-network
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- wordpress:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --staging -d your_domain -d www.your_domain
volumes:
certbot-etc:
wordpress:
dbdata:
networks:
app-network:
driver: bridge
Lưu và đóng file khi bạn đã hoàn tất chỉnh sửa.
Với các định nghĩa dịch vụ đã sẵn sàng, chúng ta sẵn sàng khởi động các container và kiểm tra yêu cầu chứng chỉ của mình.
Bước 4: Lấy chứng chỉ SSL và thông tin xác thực
Chúng ta sẽ khởi động các container bằng lệnh docker-compose up. Bằng cách thêm cờ -d, lệnh này sẽ chạy các container db, wordpress, và webserver trong nền, trong khi certbot sẽ chạy một lần và thoát.
docker-compose up -d
Bạn sẽ thấy output xác nhận các dịch vụ đã được tạo:
OutputCreating db ... done
Creating wordpress ... done
Creating webserver ... done
Creating certbot ... done
Sử dụng docker-compose ps để kiểm tra trạng thái các dịch vụ của bạn:
docker-compose ps
Khi hoàn tất, các dịch vụ db, wordpress, và webserver của bạn sẽ có trạng thái Up, và container certbot sẽ thoát với thông báo trạng thái Exit 0.
Output Name Command State Ports
-------------------------------------------------------------------------
certbot certbot certonly --webroot ... Exit 0
db docker-entrypoint.sh --def ... Up 3306/tcp, 33060/tcp
webserver nginx -g daemon off; Up 0.0.0.0:80->80/tcp
wordpress docker-entrypoint.sh php-fpm Up 9000/tcp
Bất kỳ trạng thái nào khác ngoài Up cho các dịch vụ db, wordpress, hoặc webserver, hoặc trạng thái thoát khác 0 cho container certbot đều có nghĩa là bạn cần kiểm tra log dịch vụ bằng lệnh docker-compose logs service_name.
Bây giờ bạn có thể kiểm tra xem chứng chỉ của bạn đã được gắn vào container webserver chưa bằng docker-compose exec:
docker-compose exec webserver ls -la /etc/letsencrypt/live
Nếu yêu cầu chứng chỉ thành công, bạn sẽ thấy output sau:
Outputtotal 16
drwx------ 3 root root 4096 May 10 15:45 .
drwxr-xr-x 9 root root 4096 May 10 15:45 ..
-rw-r--r-- 1 root root 740 May 10 15:45 README
drwxr-xr-x 2 root root 4096 May 10 15:45 your_domain
Khi đã biết yêu cầu chứng chỉ của mình sẽ thành công, bạn có thể chỉnh sửa định nghĩa dịch vụ certbot để loại bỏ cờ --staging.
Mở docker-compose.yml:
nano docker-compose.yml
Hãy tìm đến phần trong file có định nghĩa service certbot, sau đó thay thế cờ --staging trong tùy chọn command bằng cờ --force-renewal. Tùy chọn này sẽ yêu cầu Certbot cấp mới một chứng chỉ với các domain giống như chứng chỉ hiện có.
Dưới đây là phần định nghĩa service certbot sau khi đã cập nhật cờ này:
...
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- wordpress:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --force-renewal -d your_domain -d www.your_domain
...
Bây giờ bạn có thể chạy docker-compose up để tạo lại container certbot. Chúng ta sẽ thêm tùy chọn --no-deps để báo cho Compose rằng nó có thể bỏ qua việc khởi động dịch vụ webserver, vì nó đã chạy rồi:
docker-compose up --force-recreate --no-deps certbot
Output sau cho thấy yêu cầu chứng chỉ của bạn đã thành công:
OutputRecreating certbot ... done
Attaching to certbot
certbot | Saving debug log to /var/log/letsencrypt/letsencrypt.log
certbot | Plugins selected: Authenticator webroot, Installer None
certbot | Renewing an existing certificate
certbot | Performing the following challenges:
certbot | http-01 challenge for your_domain
certbot | http-01 challenge for www.your_domain
certbot | Using the webroot path /var/www/html for all unmatched domains.
certbot | Waiting for verification...
certbot | Cleaning up challenges
certbot | IMPORTANT NOTES:
certbot | - Congratulations! Your certificate and chain have been saved at:
certbot | /etc/letsencrypt/live/your_domain/fullchain.pem
certbot | Your key file has been saved at:
certbot | /etc/letsencrypt/live/your_domain/privkey.pem
certbot | Your cert will expire on 2019-08-08. To obtain a new or tweaked
certbot | version of this certificate in the future, simply run certbot
certbot | again. To non-interactively renew *all* of your certificates, run
certbot | "certbot renew"
certbot | - Your account credentials have been saved in your Certbot
certbot | configuration directory at /etc/letsencrypt. You should make a
certbot | secure backup of this folder now. This configuration directory will
certbot | also contain certificates and private keys obtained by Certbot so
certbot | making regular backups of this folder is ideal.
certbot | - If you like Certbot, please consider supporting our work by:
certbot |
certbot | Donating to ISRG / Let's Encrypt: <https://letsencrypt.org/donate>
certbot | Donating to EFF: <https://eff.org/donate-le>
certbot |
certbot exited with code 0
Với chứng chỉ đã có, chúng ta có thể tiếp tục sửa đổi cấu hình Nginx để bao gồm SSL.
Bước 5: Sửa đổi cấu hình Web Server và định nghĩa dịch vụ
Để bật SSL trong cấu hình Nginx, chúng ta cần thêm một chuyển hướng HTTP sang HTTPS, chỉ định vị trí chứng chỉ và khóa SSL của bạn, và thêm các tham số và header bảo mật.
Vì chúng ta sẽ tạo lại dịch vụ webserver để bao gồm các thay đổi này, bạn có thể dừng nó ngay bây giờ:
docker-compose stop webserver
Trước khi sửa đổi file cấu hình, hãy lấy tham số bảo mật Nginx được khuyến nghị từ Certbot bằng curl:
curl -sSLo nginx-conf/options-ssl-nginx.conf <https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf>
Lệnh này sẽ lưu các tham số này vào một file có tên options-ssl-nginx.conf, nằm trong thư mục nginx-conf.
Tiếp theo, xóa file cấu hình Nginx bạn đã tạo trước đó:
rm nginx-conf/nginx.conf
Tạo và mở một phiên bản khác của file:
nano nginx-conf/nginx.conf
Thêm đoạn mã sau vào file để chuyển hướng HTTP sang HTTPS và thêm thông tin xác thực SSL, giao thức và header bảo mật. Hãy nhớ thay thế your_domain bằng tên miền của bạn:
server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
rewrite ^ https://$host$request_uri? permanent;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your_domain www.your_domain;
index index.php index.html index.htm;
root /var/www/html;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
include /etc/nginx/conf.d/options-ssl-nginx.conf;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# enable strict transport security only if you understand the implications
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \\\\.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\\\\.php)(/.+)$;
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\\\\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off; access_log off;
}
location = /robots.txt {
log_not_found off; access_log off; allow all;
}
location ~* \\\\.(css|gif|ico|jpeg|jpg|js|png)$ {
expires max;
log_not_found off;
}
}
Khối HTTP server chỉ định webroot cho các yêu cầu gia hạn chứng chỉ của Certbot tới thư mục .well-known/acme-challenge. Nó cũng bao gồm một rewrite directive để chuyển hướng các yêu cầu HTTP từ thư mục gốc sang HTTPS.
Khối HTTPS server kích hoạt SSL và HTTP/2. Khối này cũng bao gồm đường dẫn đến chứng chỉ SSL và khóa bí mật (SSL key), cùng với các tham số bảo mật được khuyến nghị bởi Certbot, mà bạn đã lưu trong file nginx-conf/options-ssl-nginx.conf.
Ngoài ra, nó còn khai báo một số HTTP security headers giúp bạn đạt mức đánh giá A trên các công cụ kiểm thử như SSL Labs và Security Headers. Các header này bao gồm:
X-Frame-OptionsX-Content-Type-OptionsReferrer-PolicyContent-Security-PolicyX-XSS-Protection
Header HTTP Strict Transport Security (HSTS) hiện đang bị comment bạn chỉ nên bật nó nếu đã hiểu rõ tác động và kiểm tra chức năng của nó.
Trong khối này cũng có các directive root và index, cùng với những location block dành riêng cho WordPress đã được đề cập ở Bước 1.
Sau khi chỉnh sửa xong, hãy lưu và đóng file.
Trước khi tạo lại service webserver, bạn cần thêm ánh xạ cổng 443 vào định nghĩa service này.
Mở file docker-compose.yml của bạn:
nano docker-compose.yml
Trong định nghĩa dịch vụ webserver, thêm ánh xạ cổng sau:
...
webserver:
depends_on:
- wordpress
image: nginx:1.15.12-alpine
container_name: webserver
restart: unless-stopped
ports:
- "80:80"
- "443:443" # Thêm dòng này
volumes:
- wordpress:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- app-network
Đây là file docker-compose.yml hoàn chỉnh sau các chỉnh sửa:
version: '3'
services:
db:
image: mysql:8.0
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MYSQL_DATABASE=wordpress
volumes:
- dbdata:/var/lib/mysql
command: '--default-authentication-plugin=mysql_native_password'
networks:
- app-network
wordpress:
depends_on:
- db
image: wordpress:5.1.1-fpm-alpine
container_name: wordpress
restart: unless-stopped
env_file: .env
environment:
- WORDPRESS_DB_HOST=db:3306
- WORDPRESS_DB_USER=$MYSQL_USER
- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD
- WORDPRESS_DB_NAME=wordpress
volumes:
- wordpress:/var/www/html
networks:
- app-network
webserver:
depends_on:
- wordpress
image: nginx:1.15.12-alpine
container_name: webserver
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- wordpress:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
- certbot-etc:/etc/letsencrypt
networks:
- app-network
certbot:
depends_on:
- webserver
image: certbot/certbot
container_name: certbot
volumes:
- certbot-etc:/etc/letsencrypt
- wordpress:/var/www/html
command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --force-renewal -d your_domain -d www.your_domain
volumes:
certbot-etc:
wordpress:
dbdata:
networks:
app-network:
driver: bridge
Lưu và đóng file khi bạn đã hoàn tất chỉnh sửa.
Tạo lại dịch vụ webserver:
docker-compose up -d --force-recreate --no-deps webserver
Kiểm tra các dịch vụ của bạn bằng docker-compose ps:
docker-compose ps
Output sẽ chỉ ra rằng các dịch vụ db, wordpress, và webserver của bạn đang chạy:
Output Name Command State Ports
----------------------------------------------------------------------------------------------
certbot certbot certonly --webroot ... Exit 0
db docker-entrypoint.sh --def ... Up 3306/tcp, 33060/tcp
webserver nginx -g daemon off; Up 0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp
wordpress docker-entrypoint.sh php-fpm Up 9000/tcp
Với các container đang chạy, bạn có thể hoàn tất cài đặt WordPress thông qua giao diện web.
Bước 6: Hoàn tất cài đặt thông qua giao diện Web
Với các container của bạn đang chạy, chúng ta sẽ hoàn tất cài đặt WordPress thông qua giao diện web.
Trong trình duyệt web của bạn, điều hướng đến tên miền của máy chủ của bạn. Hãy nhớ thay thế your_domain bằng tên miền của riêng bạn:
https://your_domain
Bạn sẽ thấy trang chọn ngôn ngữ của WordPress.

Sau khi nhấp vào Continue , bạn sẽ đến trang thiết lập chính, nơi bạn cần chọn tên cho trang web của mình và tên người dùng. Tốt nhất bạn nên chọn một tên người dùng dễ nhớ (thay vì “admin”) và một mật khẩu mạnh. Bạn có thể sử dụng mật khẩu mà WordPress tự động tạo hoặc tạo mật khẩu của riêng mình.
Cuối cùng, bạn cần nhập địa chỉ email của mình và quyết định xem bạn có muốn các công cụ tìm kiếm không lập chỉ mục trang web của bạn hay không:

Nhấp vào Install WordPress (Cài đặt WordPress) ở cuối trang sẽ đưa bạn đến một lời nhắc đăng nhập:

Sau khi đăng nhập, bạn sẽ có quyền truy cập vào bảng điều khiển quản trị WordPress:

Với việc cài đặt WordPress đã hoàn tất, chúng ta có thể thực hiện các bước để đảm bảo rằng các chứng chỉ SSL của bạn sẽ tự động gia hạn.
Bước 7: Tự động gia hạn chứng chỉ SSL
Chứng chỉ Let’s Encrypt có hiệu lực trong 90 ngày. Chúng ta cần thiết lập một quy trình gia hạn tự động để đảm bảo rằng chúng không bị hết hạn. Một cách để làm điều này là tạo một tác vụ với tiện ích lập lịch cron. Trong ví dụ sau, chúng ta sẽ tạo một cronjob để định kỳ chạy một script sẽ gia hạn chứng chỉ của bạn và tải lại cấu hình Nginx.
Đầu tiên, hãy mở một script có tên ssl_renew.sh:
nano ssl_renew.sh
Thêm đoạn mã sau vào script để gia hạn chứng chỉ và tải lại cấu hình máy chủ web của bạn. Hãy nhớ thay thế sammy bằng tên người dùng non-root của bạn:
#!/bin/bash
COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"
cd /home/sammy/wordpress/
$COMPOSE run certbot renew --dry-run && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af
Script này trước tiên gán binary docker-compose vào một biến có tên COMPOSE, đồng thời chỉ định tùy chọn –no-ansi, giúp chạy các lệnh docker-compose mà không có ký tự điều khiển ANSI. Sau đó, nó cũng thực hiện tương tự với binary docker. Cuối cùng, script chuyển đến thư mục dự án ~/wordpress và chạy các lệnh docker-compose sau:
- docker-compose run: Lệnh này sẽ khởi chạy một container certbot và ghi đè lệnh đã được định nghĩa trong service certbot. Thay vì dùng subcommand certonly, nó sử dụng subcommand renew, nhằm gia hạn các chứng chỉ sắp hết hạn. Ngoài ra còn có tùy chọn -dry-run để kiểm thử script.
- docker-compose kill: Lệnh này sẽ gửi tín hiệu SIGHUP đến container webserver để tải lại cấu hình Nginx.
Sau đó, script chạy lệnh docker system prune để xóa tất cả container và image không còn được sử dụng.
Đóng file khi bạn đã hoàn tất chỉnh sửa. Cấp quyền thực thi cho nó bằng lệnh sau:
chmod +x ssl_renew.sh
Tiếp theo, mở file crontab của root để chạy script gia hạn theo một khoảng thời gian được chỉ định:
sudo crontab -e
Nếu đây là lần đầu tiên bạn chỉnh sửa file này, bạn sẽ được yêu cầu chọn một trình soạn thảo.
Ở cuối file này, thêm dòng sau:
...
*/5 * * * * /home/sammy/wordpress/ssl_renew.sh >> /var/log/cron.log 2>&1
Điều này sẽ đặt khoảng thời gian chạy tác vụ là mỗi năm phút, để bạn có thể kiểm tra xem yêu cầu gia hạn của mình đã hoạt động như mong muốn hay chưa. Một file log, cron.log, được tạo để ghi lại output liên quan từ tác vụ.
Sau năm phút, kiểm tra cron.log để xác nhận xem yêu cầu gia hạn đã thành công hay chưa:
tail -f /var/log/cron.log
Output sau xác nhận gia hạn thành công (trong chế độ dry-run):
Output- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates below have not been saved.)
Congratulations, all renewals succeeded. The following certs have been renewed:
/etc/letsencrypt/live/your_domain/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Thoát bằng cách nhấn CTRL+C trong terminal của bạn.
Bạn có thể sửa đổi file crontab để đặt khoảng thời gian hàng ngày. Để chạy script mỗi ngày vào buổi trưa, ví dụ, bạn sẽ sửa đổi dòng cuối cùng của file như sau:
...
0 12 * * * /home/sammy/wordpress/ssl_renew.sh >> /var/log/cron.log 2>&1
Bạn cũng sẽ muốn loại bỏ tùy chọn --dry-run khỏi script ssl_renew.sh của mình:
#!/bin/bash
COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"
cd /home/sammy/wordpress/
$COMPOSE run certbot renew && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af
Cronjob của bạn sẽ đảm bảo rằng các chứng chỉ Let’s Encrypt của bạn không bị hết hạn bằng cách gia hạn chúng khi đủ điều kiện.
Kết luận
Trong bài blog này, chúng ta đã cùng nhau xây dựng một hệ thống WordPress đầy đủ và bảo mật bằng cách tận dụng sức mạnh của Docker Compose. Bạn đã học cách triển khai WordPress trên một kiến trúc đa container, bao gồm cơ sở dữ liệu MySQL, máy chủ web Nginx hiệu suất cao, và ứng dụng WordPress cốt lõi. Chúng ta cũng đã thành công trong việc tích hợp chứng chỉ TLS/SSL miễn phí từ Let’s Encrypt để đảm bảo an toàn cho website của bạn và thiết lập một cronjob để tự động gia hạn chứng chỉ, giúp website luôn hoạt động mượt mà và bảo mật liên tục.