❤️ AZDIGI chính thức cập nhật hệ thống blog mới hoàn chỉnh. Tuy nhiên có thể một số bài viết bị sai lệch hình ảnh, hãy ấn nút Báo cáo bài viết ở cuối bài để AZDIGI cập nhật trong thời gian nhanh nhất. Chân thành cám ơn.
Ở Bài 6, bạn đã làm quen với Docker Compose: công cụ giúp gói toàn bộ hệ thống multi-container vào một file YAML duy nhất. Bạn đã thực hành chạy Nginx + PHP-FPM, học cách viết docker-compose.yml, dùng các lệnh docker compose up/down/ps/logs.

Nhưng đó mới chỉ là bài tập. Trong bài này, mình sẽ cùng bạn deploy một ứng dụng thực tế: WordPress + MySQL + phpMyAdmin: 3 services, đầy đủ database, web app, và công cụ quản lý DB. Đây là project gần với production nhất trong serie, và bạn sẽ thấy Docker Compose thực sự mạnh mẽ khi quản lý một stack hoàn chỉnh.
Tổng quan stack: WordPress + MySQL + phpMyAdmin
Trước khi bắt tay vào viết file Compose, hãy hiểu rõ 3 thành phần trong stack:
WordPress
WordPress là CMS (Content Management System) phổ biến nhất thế giới, hơn 40% website trên internet chạy WordPress. Nó cần một database MySQL/MariaDB để lưu trữ nội dung, và một web server (Apache hoặc Nginx) để phục vụ trang web. Image wordpress:latest trên Docker Hub đã đóng gói sẵn Apache + PHP + WordPress core, bạn chỉ cần trỏ nó đến database là chạy.
MySQL
MySQL là hệ quản trị cơ sở dữ liệu quan hệ (RDBMS), nơi WordPress lưu tất cả: bài viết, trang, cài đặt, user, plugin settings. Image mysql:8.0 cho bạn một MySQL server sẵn sàng chạy, chỉ cần cấu hình password và tên database qua environment variables.
phpMyAdmin
phpMyAdmin là công cụ quản lý MySQL qua giao diện web. Thay vì phải exec vào container rồi gõ lệnh SQL, bạn mở trình duyệt, đăng nhập, và thao tác trực quan: xem bảng, chạy query, export/import database. Rất tiện cho việc debug và backup.
Sơ đồ kết nối của stack:
┌─────────────────────────────────────────────────┐
│ Docker Network │
│ (wp-network) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ MySQL │←─│ WordPress │ │ phpMyAdmin │ │
│ │ :3306 │ │ :8080→80 │ │ :8081→80 │ │
│ │ │←─│ │ │ │ │
│ └──────────┘ └──────────────┘ └────────────┘ │
│ ↑ ↑ │
│ └────────────────────────────────┘ │
│ phpMyAdmin kết nối MySQL │
└─────────────────────────────────────────────────┘
Cả 3 services đều nằm trong cùng một Docker network, nên chúng có thể giao tiếp với nhau qua tên service (không cần IP). WordPress và phpMyAdmin đều kết nối đến MySQL qua hostname mysql (chính là tên service trong Compose).
Tạo cấu trúc project
Đầu tiên, tạo thư mục project:
mkdir -p ~/wordpress-docker
cd ~/wordpress-docker
Cấu trúc project khi hoàn thành sẽ như sau:
wordpress-docker/
├── docker-compose.yml # File cấu hình chính
├── .env # Biến môi trường (secrets)
└── backups/ # Thư mục chứa backup (tạo sau)
Đơn giản vậy thôi. Không cần Dockerfile, không cần build, tất cả đều dùng image có sẵn trên Docker Hub.
Tạo file .env cho biến môi trường
Trước khi viết docker-compose.yml, mình sẽ tạo file .env để tách riêng các giá trị nhạy cảm (password, tên database) ra khỏi file Compose. Lý do:
- Bảo mật: Bạn có thể commit
docker-compose.ymllên Git mà không lộ password - Linh hoạt: Muốn đổi password? Chỉ sửa file
.env, không cần sửa compose file - Tái sử dụng: Cùng một compose file có thể chạy trên nhiều môi trường (dev, staging, production) với file
.envkhác nhau
Tạo file .env:
cat > .env << 'EOF'
# MySQL Configuration
MYSQL_ROOT_PASSWORD=SuperSecretRoot@2024
MYSQL_DATABASE=wordpress
MYSQL_USER=wpuser
MYSQL_PASSWORD=WpPass@Secure2024
# WordPress Configuration
WORDPRESS_TABLE_PREFIX=wp_
# phpMyAdmin Configuration
PMA_PORT=3306
EOF
⚠️ Quan trọng: Đây là password mẫu. Trong thực tế, bạn phải đổi thành password mạnh của riêng mình. Không bao giờ dùng password mặc định trên server production!
Nếu project có dùng Git, thêm .env vào .gitignore ngay:
echo ".env" >> .gitignore
Viết file docker-compose.yml
Đây là phần chính của bài. Mình sẽ viết file docker-compose.yml hoàn chỉnh, sau đó giải thích từng phần:
cat > docker-compose.yml << 'EOF'
services:
mysql:
image: mysql:8.0
container_name: wp-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
networks:
- wp-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
wordpress:
image: wordpress:latest
container_name: wp-app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: mysql:3306
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX}
volumes:
- wp-content:/var/www/html/wp-content
networks:
- wp-network
phpmyadmin:
image: phpmyadmin:latest
container_name: wp-phpmyadmin
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
ports:
- "8081:80"
environment:
PMA_HOST: mysql
PMA_PORT: ${PMA_PORT}
networks:
- wp-network
networks:
wp-network:
driver: bridge
volumes:
mysql-data:
wp-content:
EOF
Bây giờ mình sẽ giải thích chi tiết từng phần.
Service: mysql
mysql:
image: mysql:8.0
container_name: wp-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
networks:
- wp-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
image: mysql:8.0: Dùng MySQL phiên bản 8.0. Mình chỉ định version cụ thể thay vìlatestvì database cần ổn định , upgrade MySQL version có thể gây lỗi data.container_name: wp-mysql: Đặt tên cố định cho container, dễ nhận diện khi chạydocker ps.restart: unless-stopped: Tự restart khi container bị crash hoặc khi Docker/server reboot, trừ khi bạn chủ độngdocker stop.environment: Các biến môi trường để MySQL tự tạo database và user khi khởi động lần đầu. Giá trị được đọc từ file.envqua cú pháp${VARIABLE}.volumes: mysql-data:/var/lib/mysql: Mount named volume vào thư mục data của MySQL. Nhờ volume này, dù xoá container rồi tạo lại, data vẫn còn nguyên.networks: wp-network: Kết nối vào custom network để các service khác tìm thấy MySQL qua hostnamemysql.healthcheck: Docker sẽ định kỳ chạymysqladmin pingđể kiểm tra MySQL đã sẵn sàng chưa. Nếu MySQL chưa ready, các service phụ thuộc sẽ đợi (nhờcondition: service_healthy). Các thông số: kiểm tra mỗi 10 giây, timeout 5 giây, thử 5 lần, chờ 30 giây ban đầu để MySQL khởi động.
Service: wordpress
wordpress:
image: wordpress:latest
container_name: wp-app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: mysql:3306
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX}
volumes:
- wp-content:/var/www/html/wp-content
networks:
- wp-network
image: wordpress:latest: Image chính thức của WordPress trên Docker Hub, bao gồm Apache + PHP + WordPress core.depends_onvớicondition: service_healthy: WordPress chỉ khởi động sau khi MySQL đã healthy (healthcheck pass). Điều này quan trọng vì WordPress cần kết nối database ngay khi start , nếu MySQL chưa ready, WordPress sẽ lỗi.ports: "8080:80": Map port 8080 trên host vào port 80 trong container. Bạn sẽ truy cập WordPress tạihttp://IP:8080.WORDPRESS_DB_HOST: mysql:3306: Trỏ WordPress đến service MySQL. Ở đâymysqllà tên service trong Compose , Docker sẽ tự resolve thành IP của container MySQL nhờ internal DNS.volumes: wp-content:/var/www/html/wp-content: Mount named volume cho thư mụcwp-content(chứa themes, plugins, uploads). Đảm bảo nội dung upload không mất khi recreate container.
Service: phpmyadmin
phpmyadmin:
image: phpmyadmin:latest
container_name: wp-phpmyadmin
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
ports:
- "8081:80"
environment:
PMA_HOST: mysql
PMA_PORT: ${PMA_PORT}
networks:
- wp-network
image: phpmyadmin:latest: Image chính thức của phpMyAdmin, chạy trên Apache + PHP.depends_on: Tương tự WordPress, đợi MySQL healthy rồi mới start.ports: "8081:80": Truy cập phpMyAdmin tạihttp://IP:8081.PMA_HOST: mysql: Cho phpMyAdmin biết MySQL server nằm ở đâu (tên service).PMA_PORT: ${PMA_PORT}: Port của MySQL (mặc định 3306).
Networks và Volumes
networks:
wp-network:
driver: bridge
volumes:
mysql-data:
wp-content:
networks: wp-network: Tạo một custom bridge network. Tất cả services trong cùng network có thể giao tiếp qua tên service. Docker Compose tự tạo network mặc định, nhưng mình khai báo tường minh để rõ ràng hơn.volumes: mysql-data, wp-content: Khai báo 2 named volumes. Docker sẽ quản lý nơi lưu trữ trên host. Dữ liệu trong volumes không bị mất khi chạydocker compose down(chỉ mất khi bạn thêm flag-v).
Deploy stack
Mọi thứ đã sẵn sàng. Kiểm tra lại cấu trúc project:
ls -la ~/wordpress-docker/
Bạn sẽ thấy 2 file: docker-compose.yml và .env. Giờ deploy:
cd ~/wordpress-docker
docker compose up -d
Lần đầu chạy, Docker sẽ pull 3 images về (mysql, wordpress, phpmyadmin) nên sẽ mất vài phút tuỳ tốc độ mạng. Output sẽ trông như thế này:
[+] Running 5/5
✔ Network wordpress-docker_wp-network Created
✔ Volume "wordpress-docker_mysql-data" Created
✔ Volume "wordpress-docker_wp-content" Created
✔ Container wp-mysql Healthy
✔ Container wp-app Started
✔ Container wp-phpmyadmin Started
Kiểm tra trạng thái các container:
docker compose ps
Output:
NAME IMAGE STATUS PORTS
wp-mysql mysql:8.0 Up (healthy) 3306/tcp, 33060/tcp
wp-app wordpress:latest Up 0.0.0.0:8080->80/tcp
wp-phpmyadmin phpmyadmin:latest Up 0.0.0.0:8081->80/tcp
Cả 3 container đều Up, MySQL hiển thị healthy. Stack đã chạy!
Kiểm tra và truy cập
WordPress - http://IP:8080
Mở trình duyệt và truy cập http://<IP-VPS>:8080. Bạn sẽ thấy trang WordPress Setup Wizard: chọn ngôn ngữ, điền thông tin site (tiêu đề, username admin, password, email).
Một số lưu ý:
- Nếu bạn test trên local: truy cập
http://localhost:8080 - Nếu trên VPS: thay
<IP-VPS>bằng IP thực của VPS - Nếu không truy cập được: kiểm tra firewall đã mở port 8080 chưa (
ufw allow 8080) - Đặt password admin WordPress thật mạnh: đây là ứng dụng mở ra internet
phpMyAdmin - http://IP:8081
Truy cập http://<IP-VPS>:8081, bạn sẽ thấy trang đăng nhập phpMyAdmin. Đăng nhập bằng:
- Username:
wpuser(hoặcrootnếu cần full quyền) - Password: password tương ứng trong file
.env
Sau khi đăng nhập, bạn sẽ thấy database wordpress ở panel bên trái. Click vào sẽ thấy các bảng WordPress (wp_posts, wp_users, wp_options,...), đây là toàn bộ dữ liệu trang web của bạn.
⚠️ Lưu ý bảo mật: phpMyAdmin không nên expose ra public trên server production. Trong thực tế, bạn nên giới hạn truy cập bằng IP whitelist, VPN, hoặc chỉ dùng khi cần rồi tắt. Trong bài học này mình mở để tiện thực hành.
Quản lý stack
Stack đã chạy, giờ là lúc học cách quản lý nó hàng ngày.
Xem logs từng service
# Xem log MySQL — kiểm tra khởi động, query errors
docker compose logs mysql
# Xem log WordPress — kiểm tra Apache access/error log
docker compose logs wordpress
# Xem log phpMyAdmin
docker compose logs phpmyadmin
# Follow log real-time của tất cả services
docker compose logs -f
# Follow log của 1 service cụ thể, hiển thị 50 dòng cuối
docker compose logs -f --tail 50 wordpress
Mẹo: Khi WordPress lỗi trắng trang hoặc lỗi kết nối database, log là nơi đầu tiên bạn cần kiểm tra. Thường sẽ thấy lỗi rõ ràng như Access denied for user hoặc Connection refused.
Exec vào container
# Exec vào WordPress container
docker compose exec wordpress bash
# Bên trong container, bạn có thể:
ls /var/www/html/ # Xem source WordPress
cat wp-config.php # Xem config (DB credentials)
php -v # Kiểm tra PHP version
exit
# Exec vào MySQL container
docker compose exec mysql bash
# Hoặc chạy MySQL client trực tiếp
docker compose exec mysql mysql -u wpuser -p wordpress
# Nhập password khi được hỏi, rồi chạy SQL:
# SHOW TABLES;
# SELECT * FROM wp_options LIMIT 5;
# EXIT;
Restart service
# Restart 1 service cụ thể (không ảnh hưởng services khác)
docker compose restart wordpress
# Restart toàn bộ stack
docker compose restart
# Dừng toàn bộ stack (giữ nguyên container)
docker compose stop
# Khởi động lại stack đã stop
docker compose start
# Dừng + xoá container (data trong volumes vẫn còn)
docker compose down
# Chạy lại
docker compose up -d
Update image
Khi WordPress hoặc phpMyAdmin có phiên bản mới, cách update rất đơn giản:
# Pull image mới nhất
docker compose pull
# Recreate container với image mới (data vẫn giữ nguyên nhờ volumes)
docker compose up -d
Docker Compose sẽ chỉ recreate những container có image thay đổi, các container khác giữ nguyên. Nếu chỉ muốn update WordPress thôi:
docker compose pull wordpress
docker compose up -d wordpress
⚠️ Lưu ý: Với MySQL, không nên update bừa. Upgrade MySQL version (ví dụ từ 8.0 lên 8.4) có thể cần migration data. Luôn backup trước khi update và đọc release notes.
Backup dữ liệu
Deploy xong mà không có backup thì giống như đi xe mà không mua bảo hiểm. Mình sẽ hướng dẫn backup cả database lẫn file WordPress.
Backup MySQL database
Dùng mysqldump từ bên trong container MySQL:
# Tạo thư mục backup
mkdir -p ~/wordpress-docker/backups
# Backup database ra file SQL
docker compose exec mysql mysqldump -u wpuser -pWpPass@Secure2024 wordpress > ~/wordpress-docker/backups/wordpress-db-$(date +%Y%m%d-%H%M%S).sql
Kiểm tra file backup:
ls -lh ~/wordpress-docker/backups/
# -rw-r--r-- 1 root root 1.2M Mar 14 10:30 wordpress-db-20250314-103045.sql
Để restore backup (khi cần):
docker compose exec -T mysql mysql -u wpuser -pWpPass@Secure2024 wordpress < ~/wordpress-docker/backups/wordpress-db-20250314-103045.sql
Backup wp-content volume
Thư mục wp-content chứa themes, plugins, và ảnh upload. Backup bằng cách copy từ container ra host:
docker compose cp wordpress:/var/www/html/wp-content ~/wordpress-docker/backups/wp-content-$(date +%Y%m%d-%H%M%S)
Hoặc nén lại thành file tar.gz để tiết kiệm dung lượng:
docker compose exec wordpress tar czf - -C /var/www/html wp-content > ~/wordpress-docker/backups/wp-content-$(date +%Y%m%d-%H%M%S).tar.gz
Script backup tự động
Để không phải chạy thủ công mỗi ngày, tạo một script backup đơn giản:
cat > ~/wordpress-docker/backup.sh << 'SCRIPT'
#!/bin/bash
# WordPress Docker Backup Script
# Chạy: bash backup.sh
BACKUP_DIR="$HOME/wordpress-docker/backups"
DATE=$(date +%Y%m%d-%H%M%S)
COMPOSE_DIR="$HOME/wordpress-docker"
# Tạo thư mục backup
mkdir -p "$BACKUP_DIR"
echo "=== WordPress Docker Backup ==="
echo "Thời gian: $(date)"
# 1. Backup MySQL database
echo "[1/2] Backup database..."
cd "$COMPOSE_DIR"
docker compose exec -T mysql mysqldump -u wpuser -pWpPass@Secure2024 --single-transaction wordpress > "$BACKUP_DIR/db-$DATE.sql"
if [ $? -eq 0 ]; then
echo " ✓ Database backup: db-$DATE.sql ($(du -h "$BACKUP_DIR/db-$DATE.sql" | cut -f1))"
else
echo " ✗ Database backup FAILED!"
fi
# 2. Backup wp-content
echo "[2/2] Backup wp-content..."
docker compose exec -T wordpress tar czf - -C /var/www/html wp-content > "$BACKUP_DIR/wp-content-$DATE.tar.gz"
if [ $? -eq 0 ]; then
echo " ✓ wp-content backup: wp-content-$DATE.tar.gz ($(du -h "$BACKUP_DIR/wp-content-$DATE.tar.gz" | cut -f1))"
else
echo " ✗ wp-content backup FAILED!"
fi
# 3. Xoá backup cũ hơn 7 ngày
echo "Dọn backup cũ (>7 ngày)..."
find "$BACKUP_DIR" -name "db-*.sql" -mtime +7 -delete
find "$BACKUP_DIR" -name "wp-content-*.tar.gz" -mtime +7 -delete
echo "=== Backup hoàn tất ==="
SCRIPT
chmod +x ~/wordpress-docker/backup.sh
Chạy backup:
bash ~/wordpress-docker/backup.sh
Để backup tự động hàng ngày, thêm vào crontab:
# Mở crontab
crontab -e
# Thêm dòng này — chạy backup lúc 3:00 AM mỗi ngày
0 3 * * * /bin/bash /root/wordpress-docker/backup.sh >> /root/wordpress-docker/backups/backup.log 2>&1
⚠️ Nhắc nhở: Backup trên cùng server với data thì chưa đủ an toàn. Trong thực tế, bạn nên copy backup sang server khác hoặc cloud storage (S3, Google Drive), nhưng đó là chủ đề ngoài phạm vi bài này.
📚 Serie Docker từ A đến Z
- Bài 1: Docker là gì? Tại sao nên dùng Docker trên VPS
- Bài 2: Cài đặt Docker và Docker Compose trên VPS Ubuntu
- Bài 3: Làm quen với Docker – Các lệnh cơ bản cần biết
- Bài 4: Docker Image & Dockerfile – Tự tạo Image riêng
- Bài 5: Docker Volume & Network – Quản lý dữ liệu và mạng
- Bài 6: Docker Compose là gì? Cài đặt và cú pháp cơ bản
- Bài 7: Deploy WordPress + MySQL + phpMyAdmin bằng Docker Compose (đang đọc)
- Bài 8: Deploy LEMP Stack (Nginx + PHP-FPM + MariaDB) bằng Docker Compose
- Bài 9: Biến môi trường & file .env trong Docker Compose
- Bài 10: Reverse Proxy với Nginx Proxy Manager + SSL tự động
- Bài 11: Deploy ứng dụng Node.js / Python với Docker Compose
- Bài 12: Backup & Restore dữ liệu Docker Volume
- Bài 13: Monitoring Docker với Portainer, Uptime Kuma và cAdvisor
- Bài 14: Docker Logging – Quản lý log hiệu quả
- Bài 15: Bảo mật Docker trên VPS
- Bài 16: CI/CD đơn giản – Auto deploy với Webhook + Docker Compose
- Bài 17: Docker Compose trong thực tế – Tổng hợp project mẫu
Tổng kết
Trong bài này, bạn đã deploy thành công một stack WordPress hoàn chỉnh với Docker Compose:
Stack 3 services:
- MySQL 8.0: database với healthcheck, named volume cho persistent data
- WordPress: web app với depends_on healthy, volume cho wp-content
- phpMyAdmin: công cụ quản lý database qua web
Best practices đã áp dụng:
- Tách secrets vào file
.env: không hardcode password trong compose file - Healthcheck cho MySQL: đảm bảo WordPress chỉ start khi DB sẵn sàng
- Named volumes: dữ liệu persist qua các lần recreate container
- Custom network: isolation và service discovery qua tên
restart: unless-stopped: tự recovery khi crash- Backup script: database + wp-content, tự xoá backup cũ
Lệnh quản lý thường dùng:
docker compose up -d: deploy/start stackdocker compose ps: xem trạng tháidocker compose logs -f [service]: xem logdocker compose exec [service] bash: vào containerdocker compose pull && docker compose up -d: update imagedocker compose down: dừng stack
Đây là stack đầu tiên bạn deploy "gần production", có database, có backup, có công cụ quản lý. Bạn có thể dùng setup này để chạy blog cá nhân, website thử nghiệm, hoặc làm môi trường dev cho WordPress.
Ở Bài 8, mình sẽ nâng cấp lên một stack phức tạp hơn: LEMP Stack (Linux + Nginx + MySQL + PHP-FPM). Bạn sẽ tự build Nginx config, tách riêng web server và PHP processor, và hiểu cách các ứng dụng PHP thực tế chạy trên production, không phải Apache all-in-one nữa, mà là kiến trúc tách rời, hiệu năng cao hơn.
👈 Bài trước: Docker Compose là gì? Cài đặt và cú pháp cơ bản
👉 Bài tiếp: Deploy LEMP Stack (Nginx + PHP-FPM + MariaDB) bằng Docker Compose
Có thể bạn cần xem thêm
- Deploy LEMP Stack (Nginx + PHP-FPM + MariaDB) bằng Docker Compose
- Docker Compose là gì? Cài đặt và cú pháp cơ bản
- Deploy ứng dụng Node.js / Python với Docker Compose
- Monitoring Docker với Portainer, Uptime Kuma và cAdvisor
- Docker Compose trong thực tế – Tổng hợp project mẫu
- Docker Volume & Network – Quản lý dữ liệu và mạng
Về tác giả
Thạch Phạm
Đồng sáng lập và Giám đốc điều hành của AZDIGI. Có hơn 15 năm kinh nghiệm trong phổ biến kiến thức liên quan đến WordPress tại thachpham.com, phát triển website và phát triển hệ thống.