Có thể cài Ghost CMS production trên shared hosting cPanel hay không? Có, nếu nhà cung cấp hỗ trợ Node.js Application, Phusion Passenger và cho phép ứng dụng Node.js chạy ổn định qua một port nội bộ. Tuy nhiên, cần nói chính xác rằng đây là một cấu hình tương thích thực tế, không phải stack production được Ghost hỗ trợ chính thức như Ubuntu, NGINX, Node.js LTS và MySQL 8.
Bài viết này trình bày cách cài Ghost CMS production trên shared hosting cPanel có Node.js, CloudLinux và Passenger. Điểm được làm rõ trong phiên bản này là Application Root không bị cố định ở /home/USERNAME/ghost-app. Người dùng có thể đặt ứng dụng trong thư mục riêng của domain, chẳng hạn /home/techntn2/domain.com/ghost-app, miễn là cấu hình cPanel, Passenger, Ghost URL và filesystem path nhất quán.
Quy trình dưới đây dựa trên case study đã triển khai Ghost 6.57.1 trên shared hosting cPanel Vietnix với LiteSpeed, CloudLinux 9.8, Node.js 22.23.2, Passenger và MariaDB 10.11.18. Nếu bạn đang chuyển website Laravel cũ sang Ghost, hãy xem thêm phần hướng dẫn migrate và restore dữ liệu Laravel sang Ghost CMS.
Ghost trên shared hosting cPanel có phù hợp cho production không?
Cần phân biệt hai khái niệm: có thể chạy và được Ghost hỗ trợ chính thức. Ghost CLI được thiết kế cho stack Ubuntu, NGINX, Node.js LTS và MySQL; công cụ này cũng có các bước tự động cho NGINX, systemd, SSL và Linux user trong môi trường chuẩn [1]. Shared hosting cPanel thường thay các thành phần đó bằng LiteSpeed hoặc Apache, Passenger và giới hạn CloudLinux.
Trong case study này, Ghost vẫn hoạt động trên MariaDB 10.11.18. Tuy nhiên, tài liệu Ghost xác nhận MySQL 8 là database duy nhất được hỗ trợ trong production [2]. Vì vậy, MariaDB nên được mô tả là khả năng tương thích đã quan sát trong một môi trường cụ thể, không phải cam kết tương thích cho mọi phiên bản Ghost.
| Thành phần | Case study | Điểm cần lưu ý |
|---|---|---|
| Hosting | Shared hosting cPanel, CloudLinux 9.8 | Có giới hạn process, worker, memory và quyền hệ thống |
| Web server | LiteSpeed/Passenger | Passenger chuyển request public vào Node.js |
| Node.js | 22.23.2 | Phải phù hợp với engine của phiên bản Ghost đang cài |
| Ghost | 6.57.1 | Cài production, bỏ các bước yêu cầu root |
| Database | MariaDB 10.11.18 | Chạy được trong case study nhưng ngoài database production được Ghost hỗ trợ chính thức |
| Process manager | Passenger qua cPanel | Không dùng systemd như trên VPS |
Phân biệt public URL, Application Root và Ghost directory
Đây là phần dễ gây nhầm nhất khi triển khai Ghost trên cPanel. Một website có ít nhất bốn khái niệm khác nhau:
| Khái niệm | Ví dụ | Ý nghĩa |
|---|---|---|
| Public URL | https://domain.com | Địa chỉ người đọc và Google truy cập |
| Filesystem Application Root | /home/techntn2/domain.com/ghost-app | Thư mục chứa startup wrapper, config và Ghost install |
| Application Path trong cPanel | domain.com/ghost-app | Đường dẫn source tương đối với home directory cPanel |
| Ghost-CLI install directory | /home/techntn2/domain.com/ghost-app/current | Thư mục được truyền cho ghost install --dir current |
| Ghost content path | /home/techntn2/domain.com/ghost-app/current/content | Nơi lưu ảnh, theme, routes và dữ liệu content |
| Internal listen address | 127.0.0.1:2368 | Địa chỉ Node.js lắng nghe phía sau Passenger, không phải port public |
cPanel Application Manager tách Deployment Domain, Base Application URL và Application Path. Application Path được nhập tương đối với home directory, còn Passenger đóng vai trò process manager và reverse proxy [3]. Vì vậy, việc dùng thư mục domain.com/ghost-app không có nghĩa website sẽ tự chạy ở URL /ghost-app/; URL public vẫn do mapping của cPanel và giá trị url trong Ghost quyết định.
Layout khuyến nghị: Application Root riêng, Ghost nằm trong thư mục con
Để tránh trộn file do cPanel tạo với cấu trúc core của Ghost-CLI, nên dùng một Application Root làm lớp wrapper và cài Ghost vào thư mục con current:
/home/techntn2/domain.com/ghost-app/ # Application Root của cPanel
├── app.js # startup file cho Passenger
├── package.json # metadata Node.js tối thiểu
├── config.production.json # config Ghost, chmod 600
├── tmp/ # thư mục restart của Passenger
│ └── restart.txt # tạo khi cần restart thủ công
└── current/ # Ghost-CLI install directory
├── content/ # media, theme, routes, settings
├── current -> versions/<version> # version Ghost đang active
├── versions/ # các version Ghost đã cài
└── .ghost-cli # metadata của Ghost-CLIGhost-CLI có hỗ trợ cờ --dir để chạy lệnh trong một directory chỉ định và tạo cấu trúc gồm content, current và versions [1]. Trong layout trên, --dir current là đường dẫn tương đối với Application Root, không phải thư mục current nằm trực tiếp dưới home directory.
Chuẩn hóa biến cấu hình trước khi cài
Không nên sao chép nguyên xi /home/USERNAME/ghost-app vào mọi server. Hãy thay các giá trị dưới đây bằng thông tin thật của tài khoản cPanel:
export CPANEL_HOME="/home/techntn2"
export DOMAIN="domain.com"
export APP_ROOT="$CPANEL_HOME/$DOMAIN/ghost-app"
export APP_PATH="$DOMAIN/ghost-app"
export GHOST_DIR="$APP_ROOT/current"
export GHOST_URL="https://$DOMAIN"
export GHOST_PORT="2368"
export NODE_VERSION="22"
export GHOST_VERSION="6.57.1"
export GHOST_CONTENT="$GHOST_DIR/content"
printf 'APP_ROOT=%s\n' "$APP_ROOT"
printf 'APP_PATH=%s\n' "$APP_PATH"
printf 'GHOST_DIR=%s\n' "$GHOST_DIR"
printf 'GHOST_URL=%s\n' "$GHOST_URL"Với primary domain, bạn có thể chọn một đường dẫn như /home/techntn2/ghost-app. Với addon domain domain.com, ví dụ phù hợp là /home/techntn2/domain.com/ghost-app. Với subdomain, có thể dùng một thư mục riêng như /home/techntn2/apps/blog-ghost. Điều quan trọng là Application Path, startup file, Ghost URL và content path phải cùng trỏ về một layout.
1. Kiểm tra điều kiện của hosting
Trước khi tạo application, hãy xác nhận nhà cung cấp có các mục Setup Node.js App, Application Manager hoặc tính năng tương đương. cPanel yêu cầu hosting bật Passenger và các package Node.js phù hợp; nhà cung cấp cũng có thể giới hạn phiên bản Node.js, số process, memory, worker và quyền restart [4].
Hãy kiểm tra các điểm sau:
- Node.js version nào đang được cPanel cung cấp và có đáp ứng engine của Ghost hay không.
- Passenger có được bật cho domain hoặc addon domain hay không.
- Application Manager có cho chọn Deployment Domain, Base Application URL và Application Path riêng hay không.
- Ứng dụng có được phép lắng nghe trên
127.0.0.1và một port nội bộ hay không. - Nhà cung cấp hỗ trợ restart Passenger bằng nút Restart,
tmp/restart.txthay cơ chế riêng nào. - Giới hạn process, memory, worker và cách xem log Node.js/Passenger.
2. Tạo database và user riêng
Trong cPanel, tạo database và database user riêng cho Ghost. Không dùng lại user của Laravel, không commit password vào Git và không truyền password trực tiếp trong command line nếu shell history được lưu.
mysql -u ghost_user -p -e \
"SELECT VERSION() AS db_version, @@version_comment AS engine;"Nếu hosting chỉ cung cấp MariaDB, hãy ghi nhận đây là một quyết định tương thích có rủi ro. Ghost khuyến nghị MySQL 8 cho production; nếu website có membership, newsletter, traffic lớn hoặc yêu cầu uptime cao, VPS với stack được Ghost hỗ trợ sẽ an toàn hơn [2].
3. Tạo Node.js Application trong cPanel
Tạo application trước khi đăng ký trong Application Manager. Với ví dụ addon domain, cấu hình khái niệm sẽ là:
| Trường cPanel | Giá trị ví dụ |
|---|---|
| Deployment Domain | domain.com |
| Application Path | domain.com/ghost-app |
| Application Root đầy đủ | /home/techntn2/domain.com/ghost-app |
| Environment | Production |
| Startup File | app.js |
| Base Application URL | / hoặc giá trị tương ứng giao diện hosting yêu cầu |
Tên trường có thể khác giữa các nhà cung cấp. Hãy ưu tiên mô tả trong giao diện cPanel thực tế. cPanel mặc định khuyến nghị startup file có tên app.js vì Passenger tìm file này khi khởi chạy Node.js application [4].
Tạo thư mục và giữ quyền tối thiểu cần thiết:
mkdir -p "$APP_ROOT"
chmod 755 "$APP_ROOT"
cd "$APP_ROOT"
pwdNếu muốn đặt app dưới thư mục domain, hãy kiểm tra document root của domain. Không mặc nhiên đặt file config, database dump hoặc source nhạy cảm vào một thư mục được web server phục vụ tĩnh. Nếu application path khác document root của addon domain, HTTPS/SSL include và mapping Passenger có thể cần nhà cung cấp hosting xử lý; người dùng shared hosting không nên tự chạy lệnh rebuild Apache khi không có quyền root.
4. Xác định Node.js runtime do cPanel cung cấp
Đường dẫn virtual environment không thống nhất giữa các hosting. Vì vậy, không nên luôn dùng mẫu /home/USERNAME/nodevenv/ghost-app/22/bin/activate. Hãy lấy lệnh activate hoặc binary path từ giao diện Node.js App của cPanel.
Một ví dụ minh họa có thể là:
# Chỉ dùng sau khi đã xác nhận đường dẫn thực tế trong cPanel
source "$CPANEL_HOME/nodevenv/${DOMAIN}/22/bin/activate"
node --version
npm --version
which node
which npmNếu hosting không tạo virtual environment theo domain, hãy dùng đúng lệnh mà cPanel cung cấp hoặc binary Node.js tương ứng, chẳng hạn /opt/cpanel/ea-nodejs22/bin/node. Không đoán đường dẫn chỉ từ tên application.
5. Cài Ghost-CLI cục bộ
cd "$APP_ROOT"
npm install --no-save --ignore-scripts ghost-cli@1.30.1
./node_modules/.bin/ghost --versionCài Ghost-CLI cục bộ giúp application dùng đúng CLI trong environment của cPanel. ghost install local là development mode, thường dùng SQLite và local process manager; không dùng lệnh đó nếu mục tiêu là production [1].
6. Xử lý giới hạn worker của CloudLinux
Shared hosting có thể hiển thị nhiều CPU nhưng tài khoản vẫn bị giới hạn process hoặc thread. Trong case study, pnpm từng gặp:
ERR_WORKER_INIT_FAILED EAGAINTrước tiên, giảm concurrency nếu hosting có Corepack/pnpm:
COREPACK=/opt/alt/alt-nodejs22/root/usr/bin/corepack
"$COREPACK" pnpm config set child-concurrency 1
"$COREPACK" pnpm config set network-concurrency 1
"$COREPACK" pnpm config get child-concurrency
"$COREPACK" pnpm config get network-concurrencyNếu vẫn lỗi, kiểm tra mức parallelism:
command -v taskset
nproc
node -e "console.log({availableParallelism:require('os').availableParallelism(), cpuCount:require('os').cpus().length})"
taskset -c 0 node -e "console.log({availableParallelism:require('os').availableParallelism(), cpuCount:require('os').cpus().length})"taskset và các biến concurrency chỉ là workaround phụ thuộc môi trường. Không nên biến chúng thành yêu cầu bắt buộc của mọi shared hosting.
7. Cài Ghost production vào thư mục con của Application Root
Với Layout A, chạy installer từ APP_ROOT và truyền --dir current. Nếu APP_ROOT=/home/techntn2/domain.com/ghost-app, Ghost sẽ được cài dưới /home/techntn2/domain.com/ghost-app/current.
cd "$APP_ROOT"
(
OLD_MODE=$(stat -c '%a' "$CPANEL_HOME")
chmod 755 "$CPANEL_HOME"
trap 'chmod "$OLD_MODE" "$CPANEL_HOME"; echo "Home permission restored to $OLD_MODE"' EXIT
PNPM_CHILD_CONCURRENCY=1 \
PNPM_NETWORK_CONCURRENCY=1 \
taskset -c 0 ./node_modules/.bin/ghost install "$GHOST_VERSION" \
--dir current \
--url "$GHOST_URL" \
--port "$GHOST_PORT" \
--ip 127.0.0.1 \
--db mysql \
--dbhost localhost \
--dbuser ghost_user \
--dbname ghost_database \
--setup-linux-user=false \
--no-stack \
--no-setup \
--no-start \
--no-enable \
--no-check-mem
)Không truyền password database trực tiếp vào lệnh. Khi installer hỏi, nhập password trong Terminal. Nếu cài đặt dừng giữa chừng, đổi tên thư mục partial install thành backup rồi chạy lại sau khi xác định nguyên nhân; không xóa database hoặc thư mục content một cách vội vàng.
8. Tạo config production cho Passenger
Tạo config.production.json ở APP_ROOT, không đặt nhầm trong APP_ROOT/current nếu bạn đang dùng Layout A:
cat > "$APP_ROOT/config.production.json" <<JSON
{
"url": "https://domain.com",
"server": {
"host": "127.0.0.1",
"port": 2368
},
"database": {
"client": "mysql",
"connection": {
"host": "localhost",
"user": "ghost_user",
"password": "NHAP_TRUC_TIEP",
"database": "ghost_database"
}
},
"paths": {
"contentPath": "/home/techntn2/domain.com/ghost-app/current/content"
},
"logging": {
"level": "info",
"transports": ["stdout"]
}
}
JSON
chmod 600 "$APP_ROOT/config.production.json"
test -d "$GHOST_CONTENT" && echo 'content directory exists'
stat -c '%A %a %n' "$APP_ROOT/config.production.json"Trong bài hướng dẫn thực tế, hãy thay /home/techntn2, domain.com và password placeholder bằng giá trị của chính bạn; không copy thông tin database thật lên bài viết.
Nếu package.json do cPanel tạo thiếu metadata, bổ sung tối thiểu:
node <<'NODE'
const fs = require('fs');
const file = 'package.json';
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
pkg.name = pkg.name || 'ghost-site';
pkg.version = '6.57.1';
pkg.private = true;
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');
NODE9. Tạo startup wrapper cho Passenger
Passenger sẽ khởi chạy app.js tại Application Root:
cat > "$APP_ROOT/app.js" <<'JS'
'use strict';
process.env.NODE_ENV = 'production';
process.chdir(__dirname);
require('./current/current');
JS
cd "$APP_ROOT"
node --check app.js
node --check current/current/index.js
node -e "console.log(require.resolve('./current/current'))"require('./current/current') là đúng cho Layout A vì current/current là symlink version đang active bên trong Ghost-CLI install directory. Nếu bạn chọn cài Ghost trực tiếp tại Application Root với --dir ., phải dùng một layout khác hoàn toàn; không trộn hai cách này.
10. Restart Passenger và kiểm tra log
Sau khi tạo hoặc sửa file, dùng nút Restart trong cPanel nếu giao diện có nút này. Một số hosting cho phép tạo restart marker:
mkdir -p "$APP_ROOT/tmp"
touch "$APP_ROOT/tmp/restart.txt"cPanel ghi log Node.js trong thư mục log của application, nhưng vị trí cụ thể có thể phụ thuộc nhà cung cấp. Nếu trang trả maintenance hoặc lỗi 503, kiểm tra log Passenger/Node.js trước khi sửa database. Không tự chạy các lệnh rebuild Apache hoặc restart service hệ thống nếu bạn không có quyền root; hãy yêu cầu nhà cung cấp hỗ trợ.
Kiểm tra public URL và Admin:
curl -I "$GHOST_URL/"
curl -I "$GHOST_URL/ghost/"
curl -sS "$GHOST_URL/ghost/api/admin/site/" | head11. Xử lý migration lock khi Ghost khởi động lần đầu
Nếu Ghost đã tạo bảng nhưng dừng giữa migration, lần khởi động sau có thể báo MigrationsAreLockedError. Kiểm tra lock trước:
SELECT * FROM migrations_lock;
SELECT COUNT(*) AS migration_count FROM migrations;Chỉ mở lock khi xác định locked = 1 và released_at IS NULL:
UPDATE migrations_lock
SET locked = 0,
released_at = NOW()
WHERE lock_key = 'km01'
AND locked = 1;Không xóa bảng migrations, không xóa database và không rollback tùy tiện. Sau khi xử lý lock mồ côi, restart một process Ghost duy nhất rồi kiểm tra lại migration.
12. Cấu hình permalink root-level và 404 native
Nếu blog dùng URL dạng https://domain.com/slug-bai-viet/, cấu hình Ghost giữ permalink root-level. Trong Settings → Labs → Routes, upload routes.yaml:
routes:
collections:
/:
permalink: /{slug}/
template: index
taxonomies:
tag: /category/{slug}/
author: /author/{slug}/Tài liệu Ghost cho biết routes file nằm trong content/settings/routes.yaml và được upload từ Admin [5]. Kiểm tra cả URL hợp lệ và URL không tồn tại:
curl -I "$GHOST_URL/bai-viet-hop-le/"
curl -I "$GHOST_URL/url-khong-ton-tai/"URL không tồn tại phải trả HTTP 404 native. Không redirect mọi URL lỗi về homepage, vì cách này có thể tạo soft-404 và khiến công cụ tìm kiếm index các URL không có nội dung.
13. Checklist nghiệm thu
| Hạng mục | Điều kiện đạt |
|---|---|
| Application Path | Đúng path tương đối với home và trỏ tới Application Root đã chọn |
| Deployment Domain | Đúng primary domain, addon domain hoặc subdomain |
| Node.js | Đúng engine requirement và đúng runtime cPanel cung cấp |
| Database | Kết nối được bằng user riêng, password không lộ trong shell history |
| Config | config.production.json có mode 600 |
| Content path | Trỏ tuyệt đối tới đúng APP_ROOT/current/content trong Layout A |
| Passenger | Restart xong trả HTML của Ghost, không phải 503/maintenance |
| Admin | /ghost/ mở được và đăng nhập được |
| HTTPS | Domain addon/primary đều có SSL mapping đúng |
| Permalink | Dùng một chuẩn trailing slash duy nhất |
| URL lỗi | Trả HTTP 404 native |
| Backup | SQL dump, content storage và backup độc lập vẫn còn |
Câu hỏi thường gặp
Có bắt buộc phải dùng /home/USERNAME/ghost-app không?
Không. Đó chỉ là ví dụ. Bạn có thể dùng /home/techntn2/domain.com/ghost-app, /home/techntn2/apps/mikka-ghost hoặc thư mục khác mà tài khoản cPanel có quyền truy cập. Hãy cập nhật đồng nhất Application Path, APP_ROOT, contentPath và startup wrapper.
Đặt Ghost trong thư mục domain có làm URL thành /ghost-app/ không?
Không tự động. Tên thư mục filesystem và URL public là hai lớp khác nhau. URL phụ thuộc mapping của cPanel/Passenger và giá trị url trong Ghost. Nếu muốn chạy root domain, hãy đăng ký Base Application URL phù hợp và đặt Ghost URL là https://domain.com.
Nên cài Ghost trực tiếp tại Application Root hay trong current?
Với shared hosting cPanel, nên dùng Application Root làm wrapper và cài Ghost trong thư mục con current. Cách này tách file Passenger/cPanel khỏi cấu trúc core của Ghost-CLI và phù hợp với quy trình nâng cấp, rollback. Nếu chọn --dir ., bạn phải dùng contentPath và entrypoint của layout trực tiếp, không trộn với Layout A.
MariaDB có chạy được Ghost không?
MariaDB có thể hoạt động trong một số môi trường như case study này, nhưng Ghost chính thức hỗ trợ MySQL 8 cho production [2]. Nếu website quan trọng, hãy ưu tiên stack được Ghost hỗ trợ.
Vì sao pnpm báo ERR_WORKER_INIT_FAILED EAGAIN?
Đó thường là dấu hiệu tài khoản CloudLinux không được tạo thêm worker hoặc thread. Hãy giảm concurrency và chỉ dùng CPU affinity nếu hosting có sẵn utility này. Đây là workaround theo môi trường, không phải bước bắt buộc của Ghost.
Có nên xóa database hoặc backup sau khi Ghost chạy không?
Không. Giữ SQL dump, thư mục storage và bản backup độc lập cho đến khi đã kiểm tra đủ nội dung, media, permalink, canonical, sitemap và 404 trong một khoảng thời gian theo dõi.
Kết luận
Cài Ghost CMS production trên shared hosting cPanel có Node.js là khả thi, nhưng cần hiểu rõ giới hạn của Passenger, CloudLinux, database và quyền hệ thống. Vấn đề quan trọng nhất không phải tên thư mục ghost-app, mà là bảo đảm bốn lớp cấu hình cùng nhất quán: domain public, Application Path cPanel, Ghost-CLI directory và contentPath.
Với addon domain domain.com, layout an toàn và dễ bảo trì là /home/techntn2/domain.com/ghost-app làm Application Root, Ghost-CLI cài vào thư mục con current, Passenger khởi chạy app.js và Ghost dùng /home/techntn2/domain.com/ghost-app/current/content. Nếu website đang chuyển từ Laravel, hãy tách riêng migration và restore để kiểm tra từng giai đoạn mà không ảnh hưởng dữ liệu nguồn.