Blog

Tôi đã build personal site bằng Headless WordPress + Astro như thế nào — và những gì tôi học được

Tác giả Cậu bé chăn bò
Cậu bé chăn bò 10 phút đọc

Kiến trúc thực tế, quyết định thiết kế, và lý do tôi không dùng Docker


Trong bài Hello World trước, tôi có nhắc đến stack của site này: WordPress chạy headless, Astro làm frontend, tất cả trên một con DigitalOcean 2GB RAM ở Singapore. Bài này tôi sẽ kể chi tiết hơn — không phải tutorial copy-paste, mà là những quyết định thực sự tôi đã phải đưa ra, những chỗ tôi sai và phải làm lại, và tại sao tôi đi đến kiến trúc hiện tại.


Tại sao Headless WordPress thay vì WordPress truyền thống

Câu hỏi đầu tiên ai cũng hỏi: tại sao phức tạp vậy? Cài theme đẹp vào WordPress không xong à?

Xong. Nhưng không phải đó là mục tiêu.

Tôi làm WordPress theme development 8 năm. Tôi biết rõ WordPress render page như thế nào — PHP query database, template engine xử lý, HTML trả về browser. Tôi đã làm điều đó đủ nhiều lần để biết nó hoạt động tốt, nhưng tôi cũng biết rõ giới hạn của nó.

Headless CMS giải quyết một vấn đề cụ thể: tách biệt nơi quản lý nội dung và nơi hiển thị nội dung. WordPress vẫn là nơi tôi đăng bài, quản lý media, kiểm soát taxonomy — những thứ nó làm tốt nhất. Nhưng giao diện người dùng cuối thấy không phải do WordPress render — đó là việc của Astro.

Kết quả thực tế: người dùng nhận HTML tĩnh từ Nginx, không có PHP execution, không có database query trong request path. Tốc độ load ở mức milliseconds, không phải seconds.

Và cá nhân hơn: tôi muốn học cách một modern frontend framework hoạt động trong môi trường production thực sự — không phải localhost, không phải Vercel free tier, mà là server tôi tự quản lý từ đầu đến cuối.


Kiến trúc tổng thể

Trước khi đi vào chi tiết từng phần, đây là bức tranh toàn cảnh:

Browser
  └── Nginx (reverse proxy, SSL termination)
        ├── yourdomain.com     → serve /var/www/site/dist/ (Astro static files)
        ├── wp.yourdomain.com  → PHP-FPM → WordPress (headless backend)
        └── mail.yourdomain.com → Roundcube (webmail)

WordPress REST API
  └── /wp-json/wp/v2/posts → Astro (lúc build)

CI/CD Pipeline
  └── git push → GitHub Actions
        ├── npm run build (Astro)
        └── rsync dist/ → server

Toàn bộ stack chạy trên một VPS DigitalOcean 2 CPU, 2GB RAM, 60GB SSD, đặt tại Singapore. Không có managed service, không có Vercel, không có Cloudflare Pages — mọi thứ tôi tự cài và tự quản lý.


Những quyết định thiết kế và lý do đằng sau chúng

1. Tại sao Astro thay vì Next.js

Next.js là lựa chọn phổ biến hơn nhiều — nhu cầu tuyển dụng tại Việt Nam cho Next.js cao hơn Astro vài chục lần. Tôi biết điều đó.

Nhưng với personal site, tôi có một constraint cứng: 2GB RAM. Next.js build tốn memory đáng kể, và nếu tôi chạy SSR thì Node.js server chạy liên tục sẽ cạnh tranh RAM với WordPress, MariaDB, và mail server.

Astro được thiết kế với triết lý “zero JavaScript by default” — nó build ra HTML tĩnh, không cần runtime. Nginx serve file HTML đó trực tiếp, không có Node.js process nào chạy nền. Trên server 2GB, đây là sự khác biệt có thể tính được bằng con số.

Thêm vào đó, với background WordPress theme development, Astro cảm giác rất tự nhiên — component system đơn giản, gần với HTML/CSS thuần, không có quá nhiều magic ẩn bên dưới.

Sau này khi tôi mở rộng kỹ năng sang React, Astro là bước đệm tốt — những khái niệm như component, props, file-based routing đều có trong Astro và áp dụng được sang Next.js.

2. WordPress chạy trên subdomain riêng

WordPress không chạy ở yourdomain.com mà chạy ở wp.yourdomain.com. Người dùng không bao giờ thấy subdomain này trong quá trình dùng web — nó chỉ là backend API endpoint cho Astro gọi lúc build.

Quyết định này quan trọng hơn tôi nghĩ ban đầu. Lý do:

Bảo mật tốt hơn. wp-admin không nằm ở domain chính. Bots scan yourdomain.com/wp-admin sẽ không tìm thấy gì. Tôi có thể giới hạn access vào wp.yourdomain.com bằng IP whitelist hoặc basic auth ở Nginx level mà không ảnh hưởng gì đến frontend.

CORS rõ ràng hơn. Nginx config trên WordPress server có header Access-Control-Allow-Origin: https://yourdomain.com — chỉ cho phép Astro frontend gọi API, không ai khác.

Tách biệt hoàn toàn. Nếu sau này tôi muốn đổi frontend sang Next.js hay bất kỳ framework nào khác, WordPress backend không cần thay đổi gì.

3. Static build thay vì SSR

Astro có thể chạy ở hai chế độ: static (build ra HTML sẵn) hoặc SSR (render theo từng request). Tôi chọn static.

Lý do đơn giản: người dùng nhận file HTML tĩnh từ Nginx. Không có computation nào xảy ra khi request đến. Đây là cách serve web nhanh nhất có thể — nhanh hơn bất kỳ SSR framework nào dù được optimize tốt đến đâu.

Trade-off duy nhất: khi tôi đăng bài mới trên WordPress, website không tự cập nhật — phải có một lần build mới. Tôi giải quyết vấn đề này bằng webhook: WordPress publish post → gọi GitHub API → trigger Actions → Astro rebuild → deploy. Toàn bộ quá trình mất khoảng 2-3 phút, hoàn toàn tự động.

4. Mail server trên cùng VPS

Đây là quyết định nhiều người sẽ không đồng ý — thông thường mail server được khuyến nghị chạy riêng vì deliverability phụ thuộc nhiều vào IP reputation.

Tôi chấp nhận trade-off đó vì đây là personal site, không phải transactional email hàng nghìn người nhận. Nhu cầu của tôi đơn giản: một địa chỉ email @yourdomain.com để dùng cho liên lạc cá nhân và liên lạc nghề nghiệp.

Stack mail: Postfix xử lý SMTP, Dovecot xử lý IMAP, Roundcube làm webmail interface. Tất cả cài native trên Ubuntu, không container hóa.


Những chỗ tôi đã sai và phải làm lại

Không có dự án nào chạy đúng ngay từ đầu. Đây là những chỗ tôi đã phải quay lại sửa.

Sai 1: Quên swap memory

Con server 2GB RAM không có swap mặc định. Lần đầu chạy npm run build cho Astro trên server, process bị kill giữa chừng vì OOM (Out of Memory). Build tốn khoảng 300-400MB RAM tạm thời trong quá trình compile.

Giải pháp: tạo 2GB swap file ngay khi setup server. Bây giờ tôi làm điều này đầu tiên trước bất cứ thứ gì khác.

Bài học: build process khác với runtime. Runtime của Astro static site gần như không tốn RAM. Nhưng bản thân việc build thì tốn. Hai con số này cần được tính riêng.

Sai 2: Build Astro trên server thay vì CI

Ban đầu tôi setup pipeline theo kiểu: push code → SSH vào server → chạy git pull → chạy npm run build trên server. Điều này dẫn đến vấn đề ở trên — server OOM lúc build.

Giải pháp đúng: build trên GitHub Actions runner (không giới hạn RAM), chỉ rsync thư mục dist/ lên server. Server không cần Node.js, không cần npm, không cần build gì cả — chỉ cần Nginx serve file tĩnh.

Đây là sự khác biệt giữa “pull-based deployment” (server tự pull code về build) và “push-based deployment” (CI build xong đẩy artifact lên server). Với constraint RAM, push-based là lựa chọn duy nhất hợp lý.

Sai 3: Không tách WordPress core ra khỏi git

Lần đầu tôi tạo repo cho WordPress, tôi commit cả thư mục WordPress core vào — hàng nghìn files, repo nặng cả GB. Sai hoàn toàn.

Cấu trúc đúng: WordPress core được cài thẳng trên server, không qua git. Chỉ có theme và plugin tự viết mới nằm trong git. wp-config.php nằm trong .gitignore vì nó chứa database credentials.

Nguyên tắc: git quản lý code bạn viết, không quản lý dependency và configuration có credentials.

Sai 4: Dùng một SSH key cho cả personal và CI/CD

Ban đầu tôi dùng SSH key cá nhân làm GitHub Actions secret để deploy. Đây là security risk — nếu key bị leak, attacker có full SSH access vào server với quyền của tôi.

Giải pháp: tạo SSH key riêng chỉ cho CI/CD với user deploy có quyền hạn chế — chỉ có thể write vào thư mục /var/www/, không có sudo, không thể làm gì khác trên server.

Mỗi actor (con người, CI pipeline) nên có credential riêng với quyền hạn tối thiểu cần thiết — nguyên tắc least privilege.


Tại sao không dùng Docker

Đây là câu hỏi tôi nhận được nhiều nhất khi kể về stack này.

Docker giải quyết một vấn đề cụ thể: isolation và reproducibility — đảm bảo mọi environment chạy giống nhau, tránh xung đột giữa các service, dễ dàng scale và di chuyển.

Nhưng Docker có chi phí: Docker daemon tốn khoảng 150MB RAM, mỗi container thêm overhead, và quan trọng hơn — Docker thêm một tầng abstraction vào giữa code và hệ điều hành. Tầng đó cần được hiểu, debug, và maintain.

Với stack của tôi trên server 2GB RAM chạy một dự án duy nhất, Docker không giải quyết vấn đề nào tôi đang có — nó chỉ thêm vấn đề mới. Không có xung đột PHP version vì chỉ có một project. Không cần scale vì đây là personal site. Không cần onboard developer mới vì tôi làm một mình.

Docker phù hợp nhất khi bạn có một server lớn chạy nhiều project của nhiều team — mỗi project cần PHP version khác nhau, Node version khác nhau, và cần cô lập hoàn toàn. Hoặc khi bạn cần deploy nhanh lên server mới mà không muốn cài từng thứ một — docker compose up và xong.

Không phải scenario của tôi. Native stack trên Ubuntu 24.04 — Nginx, PHP-FPM, MariaDB, Postfix, Dovecot — cài một lần, chạy ổn định, dễ debug khi có vấn đề vì không có container layer ở giữa.


Kết quả và những gì tiếp theo

Site hiện tại load dưới 1 giây ở Việt Nam — phần lớn là vì static HTML served từ Singapore data center, không có server-side computation.

CI/CD pipeline chạy ổn định: push code theme lên GitHub, 30 giây sau site đã có version mới. Publish bài mới trên WP admin, 3 phút sau Astro đã rebuild và deploy xong.

Mail server hoạt động, tôi có khanh@yourdomain.com để dùng cho liên lạc nghề nghiệp.

Những thứ tôi muốn làm tiếp:

  • Search — Astro static site không có server-side search. Đang xem xét Pagefind, một thư viện search chạy hoàn toàn ở client-side, không cần backend.
  • Analytics không tracking — Plausible hoặc Umami self-hosted thay vì Google Analytics.
  • Comment system — có thể dùng Giscus (dựa trên GitHub Discussions) để tránh phải chạy thêm database.

Những thứ tôi không làm tiếp: thêm feature vì thêm được, nâng cấp server vì muốn có thêm RAM để làm những thứ không cần thiết, hay migrate sang stack mới vì nghe có vẻ hay.

Stack tốt là stack giải quyết được vấn đề thực sự của bạn với mức độ phức tạp thấp nhất cần thiết. Hiện tại, stack này làm đúng điều đó.


Bài tiếp theo tôi sẽ viết về cách tôi setup CI/CD pipeline từ đầu — GitHub Actions, rsync, SSH key management, và cái webhook trigger rebuild khi publish bài WordPress. Nếu bạn đang làm điều gì đó tương tự, subscribe hoặc bookmark lại.

— Khảnh, tháng 4/2026

Bình luận (0)