需求
作品集應該證明能力,而不只是列出能力。如果把它放在代管平台上,反而會把我最想展示的東西藏起來:長期維運一個穩定又安全的服務。
構想
讓主機本身成為展示:這裡寫的每一件事,都能在提供這個頁面的同一台機器上即時驗證。
我打造了什麼
- 位於巴黎區域的雲端伺服器,花了好幾天、試了 6 次才終於取得,因為那裡的免費資源非常稀少。
- 完整的網站服務架構,每個服務都在各自受限的空間中執行,單一服務出狀況也不會波及其他服務。
- 從第一晚就開始強化安全:一個晚上套用 7 項措施,過程中服務完全沒有中斷,之後再逐步持續推進。
- 自動更新:通過核准的變更會自動上線並經過健康檢查,如果新版本沒有回應,就會自動回滾。
- 資料庫持續備份,另外每晚建立快照,並依日、週、月分層保存。
- 一個獨立的探測程式,每小時檢查網站資安並公開結果,就顯示在下方。
成果
- 5由獨立探測程式每小時重跑的資安檢查
- 7一晚內完成,零停機
- 6取得伺服器前嘗試的次數
- 1 小時兩次獨立資安檢查之間的間隔
自 2026 年 8 月起,它與 EventMap 一起運行,提供這個作品集網站。下方的即時檢查結果由獨立的探測程式量測,而不是手動填寫。
親自查證
即時檢查
- TLS 已驗證
- 原始伺服器 已驗證
- 標頭 已驗證
- DNSSEC 已驗證
- 可用性 已驗證
畫面一覽
我學到了什麼
絕不能只在機器本身上測試:疊了兩層防火牆時,服務在本機可能回應得好好的,網際網路上卻沒有人連得到。另外,懂得選擇不安裝某些東西(這次是資料庫伺服器),也是一種維運能力。
技術細節 技術堆疊、架構與決策——給技術背景的讀者
技術堆疊
- Ubuntu 22.04
- Oracle Cloud ARM (Always Free)
- nginx
- Node.js + Express
- SQLite (node:sqlite)
- systemd
- fail2ban
- iptables
- Cloudflare
- GitHub Actions
- Litestream + restic
架構一覽
- 訪客的瀏覽器
- Cloudflare — TLS 與快取
- nginx — 安全標頭、速率限制
- Node.js + Express — 在伺服器端產生頁面,支援 7 種語言;以沙箱隔離的 systemd 服務
- SQLite (node:sqlite) — 專案資料與第一方造訪紀錄
- 周邊機制:GitHub Actions 測試每一次變更;VM 拉取通過驗證的版本、檢查 /health,失敗時自動回滾;資安觀測站每 30 分鐘重新檢查一次資安。
運作方式
- 4 OCPU / 24 GB 的虛擬機(VM,Ubuntu 22.04)。nginx 反向代理位於各個隔離的服務之前,每個服務都以不具 shell 的專用系統帳號執行,並設定 ProtectSystem=strict 與 NoNewPrivileges。
- 第一個晚上(2026 年 8 月 23 日):重新產生管理員密碼、在 SSH 上啟用 fail2ban、嚴格的安全標頭(CSP、HSTS、X-Frame-Options)、存取紀錄中 IP 位址的最後一段歸零、限制 journald 的保存期限、nginx 紀錄檔輪替,以及將機密資料加密備份並存放在 VM 之外。
- 接著:nginx 速率限制、停用 rpcbind、設定 swap、遠端桌面只綁定 127.0.0.1(僅能透過 SSH 通道連線)、停用 SSH 密碼驗證、以 Full (strict) 模式使用 Cloudflare Origin CA 憑證。
- 拉取式(pull-based)持續部署:推送到 master 的變更先由 GitHub Actions 驗證,再移動 prod 標籤;VM 透過 systemd 計時器拉取該版本、執行健康檢查,失敗時自動回滾。不開放任何對內連接埠,GitHub 上也不存放任何 VM 的登入憑證。
- 兩層疊加的防火牆(VM 上的 iptables 加上雲端的 Security List),這也是我從不只在機器本身上測試的原因。
- 刻意的取捨:安全性更新會自動安裝,但 VM 絕不會自行重新開機或升級主要版本。
- 只使用第一方的造訪統計:不使用第三方 cookie,IP 位址在儲存前先經過雜湊,紀錄在 12 個月後清除。
決策與取捨
- 選擇在伺服器端產生頁面,而不是在瀏覽器中載入:這樣 7 種語言的頁面都能被搜尋引擎索引,即使沒有 JavaScript 也能閱讀。
- 選擇 systemd 服務,而不是 Docker:正式環境以沙箱隔離的 systemd 單元執行,並設有 CPU 與記憶體配額。一個從未實際部署的容器,什麼也證明不了。
- 只提供一個公開的 JSON 資料來源(/api/contenus.json),由 Mindmap 重複利用,而不是再開發第二套 API:單一可信來源,不需要額外同步。
事件與修正
- 網站改版後,pull request 的自動報告因為內嵌截圖,超過了 GitHub 留言 65,536 個字元的上限:測試全部通過,發布卻失敗。改用較輕量的縮圖,並把篇幅控制在 60,000 個字元以內後解決。
- 部署後,Cloudflare 仍會持續提供舊版腳本長達 4 小時:現在每個 CSS 與 JavaScript 網址都帶有內容指紋(?v=),新版本就不會再和舊檔案混在一起。
- 在阿拉伯文版本中,像「43 → 9.9 MB」這樣的數值,會被雙向文字演算法顯示成相反的方向:現在拉丁字母的數值都會以 Unicode 控制字元隔離。
仍待補強
- 不自動重新開機(刻意的選擇)