Homelab Armbian + Debian vận hành song song
Hai máy chủ tại nhà, một board ARM và một PC Debian, cùng phục vụ trang này qua Cloudflare Tunnel, release bằng Jenkins và tự chuyển máy khi một máy sập.
- Docker
- Jenkins
- Cloudflare Tunnel
- Traefik
- Headscale
- Linux
Trang bạn đang đọc không chạy trên Vercel hay một VPS nào. Nó được trả về từ một trong hai máy chủ đặt ở nhà tôi: một board ARM cài Armbian và một PC văn phòng cài Debian. Hai máy khác kiến trúc CPU, nằm trên hai đường Internet khác nhau, nhưng chạy cùng một bản build, cùng cấu hình, và một máy đứng chờ để nhận việc khi máy kia gặp sự cố.
Sơ đồ dưới đây có ba kịch bản. Chọn từng nút để xem một lượt truy cập, một lần release và lúc một máy mất điện đi qua hệ thống như thế nào.
Vuốt ngang để xem cả hai máy ↔
- Cloudflare kết thúc TLS và giữ HTML 60 giây ở edge.
- Cả hai máy cùng mở một connector ra tunnel illyachan-web, nên router không mở port nào.
- Cloudflare dồn request vào một connector; máy còn lại đứng chờ.
- Traefik → nginx micro-cache → 2 replica. Header X-Served-By cho biết máy nào đã trả lời.
Hai máy, hai vai#
| Armbian | Debian | |
|---|---|---|
| Phần cứng | Amlogic S905X, 4 nhân Cortex-A53 | Intel Core i5-7400, 4 nhân |
| RAM | 1,8 GB, swap nén bằng zram | 15 GB, swap nén bằng zram |
| Lưu trữ | thẻ microSD 32 GB | SSD SATA 256 GB, thêm một HDD cho NAS |
| Hệ điều hành | Armbian 26.8 (trixie), kernel 6.1 | Debian 13 trixie, kernel 6.12 |
| Kiến trúc | arm64 | amd64 |
| Container | 11 | 48 |
| Vai trò chính | phục vụ web, DNS, lưu trữ Telegram | CI/CD, tự động hóa, SSO, mạng riêng, giám sát, bản sao của web |
Tới đầu tháng 10/2026, Armbian là máy chủ duy nhất. Máy chỉ cần mất điện hoặc mất mạng là trang web cũng mất theo. Tôi thêm Debian để có máy thứ hai cùng phục vụ trang này, rồi dần chuyển các việc nặng sang đó: build, tự động hóa, đăng nhập một lần. Armbian còn lại đúng vai trò nó làm tốt nhất: một cổng web nhẹ, tốn rất ít điện.
Cái gì chạy ở đâu#
Cột giữa là phần chạy song song: cùng phiên bản, cùng cấu hình, được triển khai bằng cùng một script. Hai cột hai bên là việc riêng của từng máy.
Chỉ trên Armbian
11 container · 1,8 GB RAM
- TeleDrivelưu file không giới hạn trên Telegram
- Dozzlexem log container theo thời gian thực
- cloudflaredtunnel thứ hai cho các subdomain cũ
Chạy song song trên cả hai
cùng cấu hình, khác kiến trúc
- my-portfoliotrang này: 2 replica Next.js + Go API sau nginx
- cloudflared-webmột connector của cùng tunnel illyachan-web
- Traefik v3định tuyến theo tên miền
- AdGuard HomeDNS chặn quảng cáo cho mạng nhà
- Watchtowercập nhật image, lệch giờ nhau 30 phút
- fail2ban · watchdogchặn dò mật khẩu, tự reboot khi treo
- Headscale clientcùng một mạng riêng WireGuard
Chỉ trên Debian
48 container · 15 GB RAM
- JenkinsCI/CD: build hai kiến trúc, deploy cả hai máy
- Headscalecontrol plane tự host cho mạng WireGuard
- AuthentikSSO với passkey cho mọi app nội bộ
- n8nhơn 25 workflow: email, hóa đơn, SEO, báo cáo
- Uptime Kuma25 monitor, gồm cả máy Armbian
- Portainerquản lý Docker của cả hai máy (agent trên Armbian)
- Glancebảng tổng quan homelab
- Penpotcông cụ thiết kế, đăng nhập qua SSO
- Camera AInhận diện người bằng YOLO trên iGPU
- NAS · RustDesk · Minecraftổ chung, remote desktop, server game
Một lượt truy cập đi qua những đâu#
Không mở port nào cho web. Cả hai máy chạy cloudflared, mỗi máy giữ một connector
của cùng tunnel illyachan-web. Kết nối do máy chủ mở ra ngoài, Cloudflare đẩy request
ngược vào theo đường đó, nên router ở nhà không cần mở port và IP nhà không lộ ra.
Cloudflare không chia đều request cho hai connector: nó dồn vào một máy, máy kia đứng chờ. Khi connector đang dùng rớt hẳn, request chuyển sang máy còn lại sau khoảng 10–20 giây. Đó là failover miễn phí, không cần gói Load Balancing trả phí.
Một connector còn sống chưa chắc ứng dụng sau nó còn sống. Vì vậy mỗi máy có thêm một
timer 10 giây: chỉ bật connector khi /api/healthz của chính máy đó trả lời, và tắt
connector sau hai lần kiểm tra lỗi liên tiếp. Trước khi có bước này, một lần khởi động
lại từng để lọt trang 404 ra ngoài, và một ứng dụng đã chết vẫn tiếp tục nhận request.
Sau tunnel là Traefik, rồi tới nginx làm micro-cache, rồi mới tới hai replica
(Next.js và Go API chạy chung một image). Traefik gắn thêm header X-Served-By vào
mỗi phản hồi, nên bạn có thể mở DevTools và xem ngay máy nào vừa trả trang này.
location / {
proxy_pass http://portfolio_app;
proxy_cache portfolio_pages;
proxy_cache_valid 200 301 308 60s;
# Nhiều người mở cùng một trang: chỉ một request đi vào ứng dụng.
proxy_cache_lock on;
# Ứng dụng có lỗi hẳn thì vẫn trả bản đã cache thay vì trang lỗi.
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
}Release: một tag, hai kiến trúc, hai máy#
Tôi code trên Mac. Đẩy một tag v* lên GitHub là đủ, phần còn lại do Jenkins trên
Debian làm:
- Verify: Biome, TypeScript, kiểm tra front matter của các case study.
- Kiểm tra tag nằm trên
main, rồi khóa để hai release không chạy chồng nhau. - Build hai image trong một Docker-in-Docker riêng:
amd64cho Debian,arm64cho Armbian. Dockerfile build trên kiến trúc của máy build, chỉ stage cuối chạy qua QEMU. - Nén bằng zstd, stream qua SSH trong mạng Headscale rồi
docker loadở máy đích. Không đi qua registry nào. - Trên mỗi máy,
server-deploy.shkhởi động replica mới cạnh bản cũ, chờ healthy, chuyển nginx sang, chờ request đang dở xử lý xong rồi mới tắt bản cũ. - Kiểm tra
/api/healthztrả đúng version mới trên cả hai máy, báo kết quả lên Telegram và ghi một GitHub Deployment.
Một máy đang tắt không chặn máy còn lại: máy kia vẫn được cập nhật, job chỉ kết thúc màu đỏ để tôi biết. Rollback là chạy lại job của tag cũ và bỏ bước build. Khi chính Jenkins không chạy được, workflow GitHub Actions bấm tay vẫn làm được cùng việc.
Mạng riêng: Headscale#
Hai máy chủ, laptop, máy Windows và điện thoại nằm chung một mạng WireGuard. Control plane là Headscale tự host trên Debian thay cho dịch vụ đám mây của Tailscale. SSH deploy, Portainer quản lý Docker của cả hai máy và các phép kiểm tra chéo đều đi trong mạng này. Khóa deploy trên mỗi máy chỉ chấp nhận kết nối từ địa chỉ của máy build.
Các trang quản trị không mở công khai: chúng nằm sau Cloudflare Access hoặc đăng nhập một lần qua Authentik với passkey.
Song song nhưng không cùng lúc#
Hai máy làm nhiều việc giống nhau, nên việc quan trọng là chúng không làm cùng lúc. Watchtower trên Armbian cập nhật lúc 04:00, trên Debian lúc 04:30: nếu chạy cùng phút, cả hai Traefik có thể khởi động lại một lượt và trang web mất cả hai đường. Các bản sao lưu trên Debian được rải cách nhau khoảng 10 phút.
tua nhanh: 6,5 giờ trong 13 giây — cùng một việc, cố ý lệch giờ
- armbianDọn cache cũ và log
- debianDọn cache cũ và log
- debianSao lưu Headscale
- debianBảo trì Jenkins: sao lưu, dọn BuildKit
- debianSao lưu Authentik (SSO)
- debianSao lưu Penpot
- armbianWatchtower cập nhật image
- debianWatchtower cập nhật image
- debianKhởi động lại Minecraft (nếu đang bật)
- debianDọn dẹp hằng ngày → báo cáo Sheets
- debianBáo cáo sức khỏe SSD
Chạy liên tục, cả ngày
- mỗi 10 giâycả hai máygỡ máy khỏi tunnel nếu portfolio không khỏe
- mỗi 30 giâycả hai máyauto-heal, đồng bộ upstream nginx, autoscale 2↔3 replica
- mỗi 5 phútdebianselfheal: khởi động lại dịch vụ nền trong danh sách cho phép
Tự giám sát, tự sửa#
Mỗi lớp xử lý một loại lỗi, lớp sau đỡ cho lớp trước:
- Container: Docker healthcheck, cộng một guard 30 giây rút replica không khỏe ra khỏi nginx trước rồi mới khởi động lại nó, thêm replica thứ ba khi CPU cao kéo dài, và không bao giờ scale khi RAM trống dưới ngưỡng.
- Dịch vụ hệ thống: trên Debian,
selfhealchạy 5 phút một lần, chỉ khởi động lại các dịch vụ có trong danh sách cho phép, tối đa 3 lần mỗi 6 giờ. Lỗi khác chỉ được ghi lại để tôi xem. - Toàn máy: watchdog phần cứng khởi động lại máy khi kernel treo.
- Bên ngoài: Uptime Kuma theo dõi 25 điểm, gồm cả máy Armbian. n8n ghi báo cáo sức khỏe, dọn dẹp và SEO vào Google Sheets; Jenkins báo release qua Telegram; Glance gom tất cả vào một trang tổng quan.
Mất điện, mất mạng#
Debian cắm qua một UPS nhỏ, đủ khoảng một tiếng rưỡi; router thì không. Một script theo dõi riêng phân biệt hai trường hợp: mất Internet thì máy vẫn chạy, còn khi router biến mất 30 phút (tức là cả khu mất điện) thì máy hẹn giờ tự bật lại qua RTC rồi tắt an toàn. Máy khởi động thẳng vào chế độ không giao diện, khoảng một phút sau là các dịch vụ đã chạy lại. Tôi đã thử khởi động lại Debian khi đang có truy cập: trang không lỗi lần nào, vì Armbian vẫn phục vụ.
Ổ SSD giá rẻ cũng cần được giữ: log hệ thống nằm trong RAM và chỉ ghi xuống đĩa mỗi giờ, journald có giới hạn dung lượng, các log không cần thiết bị hạ mức. Lượng ghi đo được giảm từ khoảng 2,5 GB xuống 0,7 GB mỗi ngày.
Không chỉ có trang web#
Phần lớn sức của Debian dành cho những việc khác:
- n8n với hơn 25 workflow đang chạy: phân loại email bằng vector trong Qdrant, đọc hóa đơn (XML, PDF, OCR), bản tin buổi sáng, lịch cúp điện, giá vàng và xăng dầu, tìm việc, theo dõi SEO của chính trang này.
- Camera AI: nhận diện người bằng YOLO trên iGPU qua OpenVINO. Một bước lọc chuyển động đặt trước mô hình, cùng việc chuyển suy luận sang iGPU, hạ mức dùng CPU từ hơn một nhân xuống khoảng một phần tư nhân.
- Authentik, Penpot, Glance, một NAS gộp ổ HDD với ổ của các máy khác, RustDesk và một server Minecraft cho cả Java lẫn Bedrock.
Bài học#
- Nội dung tĩnh thì đừng đặt timer revalidate. Khi viết bài này, workflow SEO trong n8n báo ba trang trả 503. Truy log ra nguyên nhân: các case study được revalidate mỗi 24 giờ, nên lượt quét hằng ngày làm Next.js render lại cả loạt trang MDX cùng lúc trên con CPU ARM. Máy quá tải, health check của nginx quá hạn, Traefik gỡ nginx khỏi rotation và trả 503. Nội dung nằm sẵn trong image nên giờ các trang này là tĩnh hoàn toàn, chỉ đổi khi deploy.
- Health check cũng tốn tiền. Một healthcheck viết bằng
python -c "import urllib…"tốn 350–680 ms CPU mỗi lần chạy. Đổi sang/dev/tcpcủa bash thì gần như bằng không. - Dọn cache build phải dọn đúng chỗ.
docker builder prunekhông động tới builder của buildx, còn BuildKit chỉ tự dọn khi đĩa gần đầy. Cache build đã lên gần 7 GB trước khi tôi thêm bước dọn riêng mỗi đêm. - Connector còn sống chưa phải ứng dụng còn sống. Failover chỉ có ích khi máy hỏng tự rút ra khỏi vòng phục vụ.
Kết quả#
- Hai máy, 59 container, một bản build cho hai kiến trúc CPU
- Một máy mất điện hoặc mất mạng: trang chuyển sang máy còn lại sau khoảng 10–20 giây, không cần thao tác tay
- Release bằng một
git tag, khoảng 9 phút cho cả hai máy với CI giới hạn một CPU - Không mở port nào cho web; trang quản trị nằm sau Cloudflare Access hoặc SSO
- Lượng ghi SSD giảm từ 2,5 xuống 0,7 GB mỗi ngày