Ghost CMS là một nền tảng xuất bản hiện đại, nhanh và phù hợp với blog công nghệ, nhưng cách triển khai production trên VPS khác khá nhiều so với việc chạy thử bằng một lệnh Docker đơn lẻ. Khi VPS đã cài aaPanel và aaPanel quản lý Nginx, mô hình ổn định là để Docker chạy Ghost ở một cổng nội bộ, còn Nginx của aaPanel đảm nhiệm domain, HTTPS và reverse proxy.
Đây là quy trình phù hợp cho blog công nghệ như TechNT khi muốn chủ động vận hành Ghost trên VPS.
Bài viết này tổng hợp quy trình triển khai thực tế trên Ubuntu, bao gồm cấu hình Docker Compose, biến môi trường của Ghost, database MySQL, mapping domain, SSL Let's Encrypt, các header reverse proxy bắt buộc và hai lỗi khó chịu thường gặp là Request made from incorrect origin và ECONNREFUSED ...:443. Bạn có thể thay mọi giá trị mẫu như blog.example.com, ghost_db và mật khẩu bằng thông tin của mình.
Kiến trúc mục tiêu: Internet → aaPanel Nginx :443 → 127.0.0.1:2368 → Ghost container :2368 → MySQL.1. Chuẩn bị VPS, domain và aaPanel
VPS nên chạy Ubuntu LTS, có quyền root hoặc tài khoản sudo. Trước khi cài Ghost, hãy bảo đảm DNS của domain có bản ghi A trỏ về IPv4 của VPS. Nếu dùng IPv6, bản ghi AAAA cũng phải trỏ đúng; nếu chưa kiểm soát được IPv6 thì nên tạm thời gỡ AAAA để tránh trình duyệt đi nhầm máy chủ.
Trong aaPanel, cài Docker và Compose từ mục Docker. Tài liệu aaPanel hiện mô tả các chức năng quản lý image, container, Compose, network, volume, log, backup và reverse proxy. Port 80 và 443 cần được mở trên firewall/cloud security group. Không nên expose cổng Ghost ra toàn Internet nếu Nginx trên cùng VPS đã làm reverse proxy.
| Thành phần | Vai trò | Giá trị mẫu |
|---|---|---|
| Domain | URL public của blog | blog.example.com |
| aaPanel Nginx | SSL và reverse proxy | 80/443 |
| Ghost container | Ứng dụng CMS | 127.0.0.1:2368 |
| Database | Lưu nội dung, user, settings | MySQL 8 hoặc dịch vụ MySQL tương thích |
| Persistent content | Lưu ảnh, theme và dữ liệu Ghost | ./data:/var/lib/ghost/content |
2. Tạo thư mục dự án và file môi trường
SSH vào VPS, tạo một thư mục riêng cho ứng dụng. Không đặt mật khẩu thật trực tiếp trong bài viết hoặc commit vào Git.
sudo mkdir -p /opt/ghost-aapanel
sudo chown -R $USER:$USER /opt/ghost-aapanel
cd /opt/ghost-aapanel
mkdir -p data
nano .envNội dung .env tối thiểu có thể như sau:
GHOST_DOMAIN=blog.example.com
GHOST_PORT=2368
MYSQL_HOST=172.17.0.1
MYSQL_DATABASE=ghost
MYSQL_USER=ghost
MYSQL_PASSWORD=THAY_BANG_MAT_KHAU_DAI_VA_NGAU_NHIEN
Nếu MySQL chạy trong một Compose project khác, hãy dùng tên service hoặc network chung thay vì IP tĩnh. Nếu MySQL chạy trên host, địa chỉ kết nối phải phù hợp với bind address và firewall của MySQL. Ghost production yêu cầu database production; tài liệu Ghost và Docker Official Image đều dùng MySQL cho mô hình này.
3. Tạo Docker Compose cho Ghost production
Tạo docker-compose.yml. Có thể ghim major/minor version thay vì dùng latest để tránh nâng cấp ngoài ý muốn. Khi nâng Ghost, nên đọc release notes, backup trước và nâng từng minor version một cách có kiểm soát.
services:
ghost:
image: ghost:6-alpine
container_name: ghost-blog
restart: unless-stopped
ports:
- "127.0.0.1:${GHOST_PORT}:2368"
environment:
NODE_ENV: production
url: https://${GHOST_DOMAIN}
database__client: mysql
database__connection__host: ${MYSQL_HOST}
database__connection__user: ${MYSQL_USER}
database__connection__password: ${MYSQL_PASSWORD}
database__connection__database: ${MYSQL_DATABASE}
extra_hosts:
- "${GHOST_DOMAIN}:host-gateway"
volumes:
- ./data:/var/lib/ghost/content
Hai điểm trong Compose đặc biệt quan trọng. Thứ nhất, url phải là URL public có https://, không phải http://localhost:2368. Ghost cho phép override cấu hình bằng environment; với cấu hình lồng nhau, dùng hai dấu gạch dưới như database__connection__host. Thứ hai, bind port vào 127.0.0.1 để chỉ Nginx trên host truy cập được.
Directive extra_hosts không phải lúc nào cũng cần. Nó hữu ích khi Ghost phải tự gọi URL public của mình, chẳng hạn lúc xử lý kích thước ảnh hoặc webhook, nhưng DNS trong container lại resolve domain về IP của chính container. Docker Compose hỗ trợ giá trị đặc biệt host-gateway để ánh xạ hostname tới gateway của Docker host.
4. Kiểm tra Compose và khởi động Ghost
Trước khi chạy, hãy render cấu hình cuối cùng để phát hiện lỗi YAML hoặc biến môi trường rỗng.
cd /opt/ghost-aapanel
docker compose config
docker compose pull
docker compose up -d
docker compose psKiểm tra environment và log:
docker exec -it ghost-blog env | grep -E '^(url|NODE_ENV)='
docker logs --tail 100 ghost-blog
docker exec -it ghost-blog getent hosts ${GHOST_DOMAIN}Log tốt thường có dạng Ghost is running in production và Your site is now available on https://blog.example.com/. Không nên dùng việc đọc trực tiếp một đường dẫn module nội bộ của Ghost để kết luận cấu hình, vì đường dẫn này có thể thay đổi giữa các phiên bản. Environment, log và HTTP response là các kiểm tra bền vững hơn.
5. Tạo domain và reverse proxy trong aaPanel
Trong aaPanel, tạo website cho domain hoặc vào Website → Proxy tùy phiên bản giao diện. Thêm reverse proxy cho toàn bộ đường dẫn /, trỏ tới:
http://127.0.0.1:2368Tài liệu aaPanel mô tả proxy project là lớp chuyển tiếp request đến dịch vụ đích và hiện hỗ trợ Nginx. Nếu container đã map port về host, aaPanel có thể proxy đến port local đó. Không nên trỏ vào IP nội bộ của container vì IP có thể thay đổi sau mỗi lần recreate.
Trong phần cấu hình Nginx tùy chỉnh của website, bảo đảm location proxy có các header sau:
location / {
proxy_pass http://127.0.0.1:2368;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Trong đó Host giữ hostname người dùng truy cập, còn X-Forwarded-Proto cho Ghost biết request ban đầu là HTTPS dù kết nối từ Nginx đến container là HTTP. Thiếu header này là một nguyên nhân phổ biến của lỗi origin hoặc redirect sai giao thức.
6. Bật SSL Let's Encrypt và mở Ghost Admin
Trong aaPanel, mở phần SSL của website, chọn Let's Encrypt và thực hiện xác thực domain. DNS phải trỏ đúng VPS và Nginx phải phục vụ được domain trước khi xác thực. Sau khi cấp chứng chỉ, bật HTTPS và cấu hình redirect HTTP sang HTTPS nếu phù hợp.
Kiểm tra cú pháp rồi reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
curl -I https://blog.example.com/
curl -I https://blog.example.com/ghost/Nếu trang phản hồi bình thường, mở https://blog.example.com/ghost/ để tạo tài khoản quản trị. Ghost cũng cần cấu hình mail production cho lời mời thành viên, đặt lại mật khẩu và email giao dịch; hãy hoàn tất SMTP trước khi dùng các tính năng membership hoặc newsletter.
7. Sửa lỗi Request made from incorrect origin
Lỗi này thường xuất hiện khi Ghost đã biết URL public nhưng reverse proxy truyền host hoặc scheme không chính xác. Kiểm tra:
docker exec -it ghost-blog env | grep -E '^(url|NODE_ENV)='
grep -R -nE 'proxy_pass|proxy_set_header|server_name' /www/server/panel/vhost/nginx/
sudo nginx -t
sudo systemctl reload nginxEnvironment phải cho thấy url=https://blog.example.com và NODE_ENV=production. Trong Nginx, ưu tiên proxy_set_header Host $host; và bắt buộc có proxy_set_header X-Forwarded-Proto $scheme;. Sau khi reload, mở cửa sổ riêng tư hoặc hard refresh rồi thử lại /ghost/.
Không nên sửa config.production.json bằng sed bên trong container. File đó có thể vẫn hiển thị giá trị mặc định như http://localhost:2368, trong khi environment đã override lúc runtime. Cấu hình persistent nên nằm trong Compose hoặc file .env được bảo vệ quyền truy cập.
8. Sửa lỗi ECONNREFUSED và IMAGE_SIZE_URL do Docker hairpin DNS
Một lỗi khó nhận ra là Ghost tự gọi https://blog.example.com từ bên trong container, nhưng DNS lại trả về IP của chính container, ví dụ 172.18.0.3. Container sau đó cố kết nối đến 172.18.0.3:443, trong khi HTTPS thực tế đang lắng nghe ở Nginx trên host. Kết quả là ECONNREFUSED, đôi khi kéo theo lỗi lấy kích thước ảnh hoặc webhook.
Kiểm tra DNS từ container:
docker exec -it ghost-blog getent hosts blog.example.comNếu kết quả là IP của chính container, giữ dòng sau trong Compose:
extra_hosts:
- "blog.example.com:host-gateway"Recreate container:
docker compose config
docker compose up -d --force-recreate
docker exec -it ghost-blog getent hosts blog.example.com
docker logs --tail 100 ghost-blog | grep -Ei 'ECONNREFUSED|IMAGE_SIZE_URL|ERROR'Kết quả mong đợi là domain resolve về gateway của Docker host, chẳng hạn 172.17.0.1, không còn là IP của Ghost container. Nếu lỗi chuyển sang ActivityPub hoặc webhook nhưng trang chủ và /ghost/ vẫn hoạt động, hãy tách riêng hai vấn đề: lỗi đó không đồng nghĩa reverse proxy đang hỏng.
9. Checklist backup, bảo mật và nâng cấp
Volume ./data chứa content, ảnh và theme nên được backup định kỳ. Database cũng phải có bản dump riêng; backup volume không thay thế backup MySQL. Trước khi rebuild, uninstall hoặc xóa container trong aaPanel, hãy dùng chức năng backup/restore của aaPanel hoặc sao chép dữ liệu ra nơi khác.
| Hạng mục | Khuyến nghị |
|---|---|
| Mật khẩu | Dùng chuỗi ngẫu nhiên dài, lưu trong .env có quyền 600 và không commit Git. |
| Port Ghost | Bind vào 127.0.0.1, chỉ public qua Nginx. |
| HTTPS | Dùng Let's Encrypt hoặc chứng chỉ hợp lệ, redirect HTTP nếu cần. |
| Database | Backup định kỳ, không đổi biến password sau khi database đã khởi tạo nếu chưa đồng bộ dữ liệu thật. |
| Nâng cấp | Đọc release notes, backup, pull image và recreate có kiểm soát; tránh nhảy major version đột ngột. |
| Cấu hình SMTP trước khi bật membership/newsletter. | |
| Log | Kiểm tra docker compose logs và Nginx error log sau mỗi thay đổi. |
10. Kết luận
Cài Ghost CMS trên VPS Ubuntu có aaPanel không khó nếu phân tách đúng trách nhiệm: Docker chạy ứng dụng và lưu dữ liệu persistent, aaPanel Nginx quản lý domain và TLS, còn Ghost nhận URL public bằng environment. Ba chi tiết quyết định độ ổn định là url HTTPS, các header Host/X-Forwarded-Proto và xử lý host-gateway khi container cần tự truy cập domain của chính nó.
Nếu triển khai mới bằng bộ công cụ Docker chính thức của Ghost 6, bạn cũng nên đọc hướng dẫn Docker Compose mới của Ghost, vì bộ công cụ đó có thể dựng thêm Caddy, MySQL và các dịch vụ mở rộng. Tuy nhiên, với VPS đã dùng aaPanel Nginx, mô hình reverse proxy đến 127.0.0.1:2368 ở bài này là cách trực quan, dễ kiểm tra và phù hợp khi aaPanel đang quản lý SSL.
Câu hỏi thường gặp
Có nên dùng localhost trong url của Ghost không?
Không khi chạy production. url phải là địa chỉ người dùng thực sự truy cập, thường là URL HTTPS của domain.
Vì sao file config vẫn hiện localhost sau khi đã đặt url trong Compose?
Ghost có thể tạo file cấu hình mặc định trong image, sau đó override bằng environment lúc runtime. Hãy kiểm tra environment và log thay vì sửa file trong container.
aaPanel có thay thế Docker không?
Không. aaPanel là lớp quản trị máy chủ và Nginx; Docker vẫn là lớp chạy container. Hai thành phần bổ trợ nhau trong kiến trúc này.
Nguồn tham khảo: Ghost Configuration, Ghost Docker installation, Docker Official Image for Ghost, Docker Compose Networking, aaPanel Docker documentation và aaPanel Proxy Project.