Dev Dock Manager 的前世今生:為什麼要把能用的專案打掉重練
在我們跳進那些充滿 unwrap()、borrow checker 和 React Hooks 的技術細節之前,我們先來聊聊這個故事的主角:Dev Dock Manager,以及它背後的靈魂伴侶 Dev Dock。

Dev Dock Manager 的前世今生:為什麼要把能用的專案打掉重練

在我們跳進那些充滿 unwrap()、borrow checker 和 React Hooks 的技術細節之前,我們先來聊聊這個故事的主角:Dev Dock Manager,以及它背後的靈魂伴侶 Dev Dock。
這是什麼東西?
首先,你得知道我有一個專案叫做 Dev Dock GitHub。 它的功能很硬核:
幫你 Build 出一個「有圖形介面 (GUI)」的 Docker Image。

想像一下,你想要一個乾淨的 Ubuntu 環境,裡面有桌面、有 VNC Server、有 NoVNC,還有一堆開發工具,隨時隨地都能連進去做一堆有的沒的。 Dev Dock 就是負責把這些東西打包成一個 Docker Image 的工具。
但是,有了 Image 之後,要跑起來卻很麻煩。 你得記住一長串指令:
docker run -d -p 5901:5901 -p 6901:6901 -v /my/code:/workspace ...
Dev Dock Manager GitHub 就是為了解決這個痛點而生的「專屬控制台」。
它讓整個流程變成了「點一下」的事:
- 它能辨識由 Dev Dock 建置出來的 GUI Image。
- 它幫你管理這些特殊的容器(Container),不用再手打那一長串 Port Mapping 參數。
- 最重要的是,它直接整合了 Web Terminal 和 noVNC,讓你打開瀏覽器,就像打開了一台雲端電腦或是虛擬機。
聽起來很棒對吧?舊的Django 版本也確實做到了這些功能。 它穩定、可靠,就像那台你大學時期用到爛的舊筆電。
那,為什麼我要把它整個打掉重練,換成 Rust + React?
修改動機一:從「填表單」到「現代化工具」的 UX 焦慮
舊版的介面是標準的 Django Template SSR(Server-Side Rendering)。 這在快速開發上很棒,但它的互動體驗就像是在填寫每年四月報稅單——功能都有,但你不會想多看它一眼。

1. 面對「虛無」的恐懼
在舊版中,如果你的首頁裡沒有任何 Container,使用者看到的是一個空蕩蕩的表格。 這在工程師眼裡是「正確的」(Query result = 0),但在使用者眼裡是「壞掉了嗎?」還是「我現在要幹嘛?」。 這種沈默的 UI 對想嘗試這個專案的人來說極度不友善。
2. 資訊過載的表格
以前我為了讓使用者看到容器的詳情(SSH Port、Image Tag、Command),我把所有欄位都塞進同一列。 結果就是一個擁擠不堪的表格,使用者得拿出放大鏡才能找到此時此刻需要的資訊。

我們需要一個更現代的解法:React。
我們需要動態的 Modal、需要直覺的點擊反饋,而不是每次操作都要重新整理整個頁面。
修改動機二:專注於「對」的 Image
舊版的設計太過「通用」。 它試圖把自己當成一個通用的 Docker 管理器,列出所有 Image,不管那是哪個專案的。
但這個專案的核心價值其實是服務 Dev Dock 建置出來的環境。 我意識到,這個工具不能只「列出 Docker 裡的東西」,它應該要引導使用者:
- 過濾雜訊:只顯示符合 Dev Dock 規範的 Image(例如 gui-vnc 系列),不用讓使用者在一堆奇怪版本的 redis、nginx 映像檔中大海撈針。
- 版本聚合:清楚呈現 Image 的版本(Tags),讓使用者知道現在跑的是 latest 還是 v1.0。
修改動機三:為了效能(其實是為了爽)的後端重構
最後,也是最私心的理由:我想試試看 Rust。
Django 很棒,但對於這種需要大量與系統底層(Docker Socket)溝通、處理即時 WebSocket 串流(Web Terminal)的應用場景,Python 有時會顯得力不從心,或是寫起來需要太多 workaround。
我想嘗試用 Rust (Axum) 打造一個:
- 極致輕量的後端二進位檔。
- 型別安全的 WebSocket 處理(不再怕前端傳奇怪的 JSON 搞掛後端)。
- 解決 Python 在 Docker 內處理 Signal 和 Process 的一些痛點。
於是,這場從 Django SSR 遷移到 Rust + React 的大遷徙就此展開。 這不僅僅是換語言,更是一次對系統互動邏輯的深度反思。
接下來,請容許我花點時間分享這段過程中的技術細節,包括那些讓我懷疑人生的坑。
Part 1: 前端大修改 —— 從「工程師美學」到「人類友善體驗」
舊版的介面是標準的 Django Template。 你知道的,就是那種「後端工程師寫的前端」——
能跑、不會爆炸,但醜得很有個性。
這次我把前端從 Django SSR 換成了 React (Next.js),目標不只是換個框架,而是要讓使用者覺得自己是在操作一個現代化的開發工具,而不是在填公司內部奇怪的申請表單。
以下四大核心的改動:
1. 空狀態(Empty States):不再讓使用者面對「虛無」
在舊版中,當你首頁的容器空空如也時,畫面就是一個 空的 <table>。 這在技術上完全正確( *SELECT FROM containers** 回傳 0 筆),但在 UX 上這叫「被放生」。
使用者看到一片白,心裡只會有兩個想法:
- 系統壞了嗎?
- 所以我現在要幹嘛?
修改後:我們不再依賴使用者的通靈能力。如果沒有容器,會顯示一個「有意義的空狀態」:
- 訊息:「目前沒有容器喔 (No containers yet)。」
- 提示:「點下面那個按鈕,建立你的第一個 GUI 環境吧!」
- 行動:直接放一顆又大又明顯的 Create Container 按鈕。
這就像是遊戲的新手教學,我們不再假設使用者知道下一步該怎麼做,而是直接把路標建立好。

2. 表格行(Rows):停止對使用者進行視力測驗
以前為了讓使用者看到 Container 的所有參數(SSH Port、Image Tag、Command、Privileged Flags...),我把所有欄位硬塞進同一列表格裡。結果就是...
資訊爆炸
你要在一堆密密麻麻的文字中找到那個該死的 Port Mapping,簡直是在考驗眼力。
修改後: 引入了現代化 UI 的標配——點擊展開詳情 (Click-to-View Details)。
- 表格瘦身:列表只顯示最關鍵的資訊(ID、Name、Status)。
- 整行可點:除了操作按鈕外,點擊整行任一處都會彈出一個 Details Modal。
- 完整身家調查:在 Modal 裡用漂亮的排版顯示完整的 Image Tag、啟動指令、SSH 設定以及那些落落長的 Docker Flags。
既保留了表格的「一目瞭然」,又滿足了「我想看細節」的需求。

3. 映像檔(Images):從「雜物堆」變成「精品目錄」
這大概是視覺上最大的改動。 原本的 Dashboard 就像是一個沒有整理過的倉庫:
- 什麼都有:它會把 Docker 裡所有的 Image 都列出來(包含鄰居用的 Redis、Nginx)。
- 版本地獄:gui-vnc:latest 是一行,gui-vnc:v1 又是另一行。如果有 10 個版本,列表就被洗版了。
但別忘了,我們的目標是用 Dev Dock 建立的 GUI 環境。我一點都不關心隔壁老王的 Redis 長怎樣。
修改後:
- 自動過濾:後端現在只吐出符合 DOCKER_IMAGE_NAME(預設 gui-vnc)的映像檔。雜訊直接過濾掉。
- 版本聚合 (Tag Aggregation):這是最療癒的部分。我們不再用「Grid Cards」(以前看起來很亂),改用列表式的 Meta Card。

結果:多個版本,一個 Entry。乾淨、清爽,就像整理好的書架。
4. 導航列與狀態指示:斷捨離的藝術
原本的導航列把 Images 和 Containers 分成兩個獨立頁面。這導致了一個奇怪的流程:使用者得先去 Images 頁面確認有沒有 Image,再跑去 Containers 頁面建立容器。
修改後: 我意識到 Image 其實是 Container 的「前置相依」,不應該分家。
- 合併頁面:我把 Image 列表搬到了 Containers 頁面的最上方。流程變得很自然:先挑 Image -> 再開 Container。
- 簡化導航:Navbar 只剩下一個 Containers 入口。
- 狀態指示燈:原本那個佔據標題列的綠點🟩,被縮小成一個可愛的 Status Pill,安靜地躲在右上角。它還在,但不再對著使用者大吼大叫。

這一波操作下來,Dev Dock Manager 終於從一個「能用的工具」變成了一個「好看的系統」。使用者不再需要去猜測 UI 的邏輯,因為 UI 終於開始講人話了。
接下來,我們要進入真正的重頭戲——
把後端從 Django 換成 Rust。
如果不說,你可能不知道這背後有多少坑...
這篇部落格文章的第三部分,也是最坑的章節。
如果說前端改版是為了取悅使用者,那麼後端改版就是為了挑戰自我(順便被編譯器羞辱)。我從開發速度最快的 Django,切換到了以嚴格著稱的 Rust (Axum + sqlx)。
以下是踩坑實錄:
Part 2: 後端大修改 —— Django 轉 Rust 的踩坑實錄
如果不把一個運作正常的 Python 後端拆掉,換成 Rust 重寫一遍,那還叫工程師嗎?
這次我們把原本舒適圈裡的 Django 拋棄,換上大家公認的效能怪獸 Rust (Axum + sqlx + bollard)。 目標是更小的 Docker Image、更快的啟動速度,以及那種編譯通過就覺得自己無敵的快感。
結果?
我踩滿了一排坑。
坑一:SQLite 在 Docker 裡的幽靈路徑
問題: 這是一個經典的 Docker 鬼故事。我的容器裡明明有 /app/data/db.sqlite3 這個檔案,我 exec 進去用 ls 看得到,用 touch 摸得到,但 Rust 程式一跑起來就崩潰:
SqliteError { code: 14, message: "unable to open database file" }
真相: sqlx 這個套件非常嚴謹(或者是龜毛)。當你設定 DATABASE_URL=sqlite:///app/data/db.sqlite3 時,它的解析邏輯可能跟 Docker Volume 掛載的路徑認知有落差。加上我們就只是在 Dockerfile 裡 mkdir,Rust 執行當下如果沒有再次確認目錄權限或路徑,就會直接爆開。
解法: 不要相信 URL parser 自己動手豐衣足食。 我寫了一個 sqlite_absolute_path 函數,強制把各種寫法的 URL 轉成絕對路徑 /app/data/db.sqlite3 然後在建立 Pool 之前,先用 Rust 的 std::fs 強制幫它鋪路。
// 這是對付龜毛 sqlx 的最終手段
if let Some(parent) = absolute_path.parent() {
std::fs::create_dir_all(parent).ok(); // 強制先建目錄,不管有沒有
}
// 然後再連線...
坑二:API 路由的「鬼打牆」
問題: 前端呼叫 /dashboard/api/containers,Rust 後端只有 /api/containers。 Traefik 在中間轉發,但我不想在 Traefik 層寫太複雜的 Rewrite Rules,因為那樣除錯很痛苦。
解法: Rust 的 Router 很靈活,我們可以直接用 nest 把同一組 API 掛兩次。這就是所謂的「只要我夠懶,問題就追不上我」。
Router::new()
.nest("/api", api::router()) // 給我看(測試用)
.nest("/dashboard/api", api::router()) // 給前端看(正式用)
雖然看起來有點多餘,但這解決了所有路徑對齊的問題,而且零成本。
坑三:WebSocket 驗證演進史
要把 JWT Token 塞進 WebSocket 連線,我經歷了三個階段的崩潰,這大概是所有 WebSocket 開發者的必經之路:
- 第一版:塞在 Subprotocol(失敗)
- 想說老熟了,直接上 **new WebSocket(url, ["token." + jwt])**。
- 結果:瀏覽器報錯。因為標準 Base64 裡面有 + 和 =,這些字元在 HTTP Token 規範裡是違法的。瀏覽器直接不讓你連。
- 第二版:塞在 URL Query(不優雅)
ws://...?token=jwt
- 結果:能用,但醜。JWT 會出現在 Access Log、Proxy Log 裡,資安分數扣十分。
3. 最終版:先上車後補票(可能是正解)
- 連線時 URL 只帶 ID,不帶 Token。
- 連線建立後的第一則訊息,前端立刻發送 { token: "...", action: "..." }。
- 後端收到第一則訊息才做驗證,驗證不過就直接 Close(Unauthorized)。
這讓協定變得很乾淨,也沒有 URL 長度或字元限制的問題。
坑四:WebSocket 終端機為什麼「啞」了?
問題: Console 連上了,可以看到 Docker 的輸出(Stdout),但我打字(Stdin)完全沒反應。而且打 exit 之後,連線不會斷,畫面停在那邊尷尬。
真相: 這是經典的 Async/Await 陷阱。 我在處理 Docker 輸出流(Stdout -> WebSocket)時,用了 await。這導致整個 Event Loop 卡在「讀取輸出」這件事上,根本沒空去處理前端傳來的「輸入」。
解法: 不要阻塞主迴圈! 把「轉發 Docker 輸出」這個工作丟到背景去跑(tokio::spawn)。
// 正確示範:背景執行,讓主線程繼續處理 Input
tokio::spawn(forward_docker_stream_to_ws(output, ws_tx.clone()));
// 現在主迴圈可以愉快地接收前端輸入了
while let Some(msg) = ws_rx.next().await { ... }
同時,記得在 forward_docker_stream_to_ws 結束時(代表 Docker 程式跑完了),主動送一個 Close Frame 給前端,這樣 UI 才會知道要顯示「已斷線」。
結語:值得嗎?
這次重構雖然充滿了與 borrow checker 和 async runtime 的搏鬥,但結果是令人滿意的。
- 前端:從一個 CRUD 工具變成了真正的 Dashboard。
- 後端:Rust 帶來的記憶體安全與效能是無價的。二進位檔只有幾 MB,啟動只要幾毫秒,而且我知道——
只要編譯過了,它通常就不會爆。
如果你對這個過程有興趣,或者想看看那些讓我折磨自己的程式碼,歡迎到 GitHub 逛逛:GitHub: Dev Dock Manager Rust
祝大家的新年程式都能乖乖聽話,資料庫查詢永遠正確!:D
메타데이터
- post_id
- 505527832e33
- slug
- dev-dock-manager-的前世今生-為什麼要把能用的專案打掉重練-505527832e33
- url
- https://medium.com/@natlee_/dev-dock-manager-%E7%9A%84%E5%89%8D%E4%B8%96%E4%BB%8A%E7%94%9F-%E7%82%BA%E4%BB%80%E9%BA%BC%E8%A6%81%E6%8A%8A%E8%83%BD%E7%94%A8%E7%9A%84%E5%B0%88%E6%A1%88%E6%89%93%E6%8E%89%E9%87%8D%E7%B7%B4-505527832e33
- canonical_url
- https://medium.com/@natlee_/dev-dock-manager-%E7%9A%84%E5%89%8D%E4%B8%96%E4%BB%8A%E7%94%9F-%E7%82%BA%E4%BB%80%E9%BA%BC%E8%A6%81%E6%8A%8A%E8%83%BD%E7%94%A8%E7%9A%84%E5%B0%88%E6%A1%88%E6%89%93%E6%8E%89%E9%87%8D%E7%B7%B4-505527832e33
- author_url
- https://medium.com/@natlee_
- status
- ok
- fetched_at
- 2026-07-17 21:32:07