dsh headless systemd 服務設定與 SSH 隧道
在 VPS 上以專用帳號執行 dsh,固定版本並設定 Restart 規則;透過 journalctl 查閱日誌,再以 SSH 隧道安全存取 UI。
在 VPS 上以 headless 模式執行 dsh,而不是在終端機中
在 VPS 上以 headless 模式執行 dsh,只需要一個 systemd unit 檔案,以及一個專用帳號來擁有該服務。dsh 是 DeepSeek Harness 的命令列啟動器。DeepSeek Harness 是 DeepSeek 的代理程式執行環境,於 2026 年 8 月以 MIT 授權條款發布 developer preview。harness 是圍繞模型運作的程式,而不是模型本身。因此,您交由 systemd 管理的是迴圈、工具與權限,不是 DeepSeek 的推論程序。快速入門指南會要求您輸入 npx @deepseek-ai/dsh web,這是正確的做法,但您關閉 SSH (secure shell) 工作階段後,該程序也會立即終止。
unit 檔案可同時解決 4 個問題。服務會在重新開機後恢復。輸出會寫入 journal,不會持續滾過終端機畫面。服務會以非 root 帳號執行。服務使用的版本也會固定為您選定的版本。這一點在此特別重要,因為上游已明確表示:
DeepSeek Harness 目前處於 developer preview 階段,並且快速迭代。一定會發生破壞相容性的變更。
本指南假設您已能手動執行 dsh。如果還不能,請先參閱在 VPS 上安裝 DeepSeek Harness,並在 npx @deepseek-ai/dsh web 能提供頁面後再回到本指南。
先安裝 Node,因為 npm 不會發出警告
node -vUbuntu 24.04 提供的套件是 Node 18(截至 2026 年 8 月為 18.19.1)。對於今年發布的套件而言,這個版本已經過舊。@deepseek-ai/dsh 未發布 engines 欄位,因此 Node 版本過舊時,npm 不會顯示 EBADENGINE 警告。錯誤會延後到執行階段才發生,形式可能是語法錯誤或缺少內建功能。等到這時才發現問題,處理成本會高很多。請從 NodeSource 安裝目前的長期支援(LTS)版本:
curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh
less /tmp/nodesource_setup.sh
sudo -E bash /tmp/nodesource_setup.sh
sudo apt install -y nodejs
node -vnode -v 現在應該會顯示 v22 版本。加入 less 這一行,是因為直接將遠端指令碼管線傳給 bash 會執行你尚未讀過的程式碼。
先確認能執行,再撰寫 unit
npx @deepseek-ai/dsh@0.1.0-rc.7 web讓它持續執行。從第二個 SSH 工作階段執行:
curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo upup 表示 web profile 正在 loopback 上監聽,這是其預設繫結位置。curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused 表示它未在該位置監聽,而第一個終端機已說明原因。繼續之前,先以 Ctrl+C 停止手動執行:嘗試繫結已被其他程序占用的埠時,unit 會因 Error: listen EADDRINUSE: address already in use 127.0.0.1:3080 而失敗。
0.1.0-rc.7 是 18 August 2026 發布的版本。使用 npm view @deepseek-ai/dsh version 檢查目前版本,然後固定你決定執行的版本。
全域安裝指定版本
在 unit file 中使用 npx 並不正確。它會在程序啟動時解析套件版本,因此 3 個月後重新啟動時,preview-stage agent 可能載入不同的 build,而您沒有做任何變更。啟動時也必須能連線至 npm registry;registry 速度變慢的那天,原本正常的機器就可能因此使 unit 啟動失敗。請先以您記錄的版本安裝一次:
sudo npm install -g @deepseek-ai/dsh@0.1.0-rc.7
command -v dsh
npm ls -g --depth=0 @deepseek-ai/dsh若 npm 來自 NodeSource,command -v dsh 會輸出 /usr/bin/dsh;若來自 Ubuntu 自有的套件,則會輸出 /usr/local/bin/dsh。請在 unit file 中使用它實際輸出的路徑。npm ls -g 會輸出確切版本。6 週後行為發生變化,而您記不得當初安裝的版本時,這就是需要的資訊。若安裝失敗、之後 command -v dsh 沒有輸出內容,或取得的版本不是您指定的版本,請先處理dsh 安裝與版本常見故障,再建立 unit file。
擁有服務、且不擁有其他內容的使用者
此代理程式會執行 shell 命令,這就是它的工作。以 root 執行時,每次工具呼叫都會具備 root 權限,因此應為它建立專用帳號,並停用登入 shell。
sudo useradd --system --create-home --home-dir /var/lib/dsh --shell /usr/sbin/nologin dsh
sudo install -d -o dsh -g dsh -m 750 /var/lib/dsh/harness /var/lib/dsh/workspace
id dsh/var/lib/dsh/harness 會變成 DSH_HOME,也就是 dsh 儲存設定檔的目錄。設定檔是由具名的外掛程式套件組成的堆疊,最上層再加上自訂修補層;web 與 headless 設定檔會在第一次啟動時,根據隨附的範本自行建立。之後加入該堆疊的任何內容,都會以此使用者身分執行,並使用代理程式自己的檔案與 shell 存取權。因此,安裝前審查外掛程式與建立帳號一樣,都是相同工作的必要部分。第一次啟動會寫入檔案,也可能擷取套件,因此應在可監看執行狀態的情況下手動完成。
sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile web請明確設定 HOME,不要依賴 sudo 對它的處理方式,因為對非登入命令而言,是否由 sudo 重寫 HOME,取決於 /etc/sudoers 中的 set_home 設定。設定錯誤時,第一次執行會將快取目錄寫入由 dsh 擁有的 你的 主目錄,服務之後便無法找到自己的狀態資料。等到 curl 檢查傳回 up 後,按下 Ctrl+C 停止它。
單元檔案
寫入 /etc/systemd/system/dsh.service:
[Unit]
Description=DeepSeek Harness (dsh) web profile
Documentation=https://github.com/deepseek-ai/deepseek-harness
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
User=dsh
Group=dsh
WorkingDirectory=/var/lib/dsh/workspace
Environment=HOME=/var/lib/dsh
Environment=DSH_HOME=/var/lib/dsh/harness
ExecStart=/usr/bin/dsh --profile web
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
SyslogIdentifier=dsh
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.targetExecStart= 會使用從 command -v dsh 取得的絕對路徑。systemd 會針對不含路徑的命令名稱搜尋固定的路徑清單,但該清單不是 shell 的 PATH。因此,使用絕對路徑可以避免猜測。
WorkingDirectory= 是相對路徑解析的位置,也是未提供引數時,執行 ls 的工具呼叫起始的位置。請將它設為提供給 agent 的工作區路徑。如果目錄不存在,或服務使用者無法進入該目錄,單元會先以 status=200/CHDIR 失敗,根本不會執行 dsh。
ProtectHome=true 會將 /home 和 /root 隱藏於程序。此處這樣設定是安全的,因為服務存取的所有內容都位於 /var/lib/dsh 下。請將工作區設為 /home 下的路徑,否則 agent 會回報目錄不存在;記住這一行後,才能理解原因。ProtectSystem=full 會讓 /usr、/boot 和 /etc 只能讀取,而服務不需要寫入這些位置。
進一步加強限制很容易令人想這樣做,但通常是不正確的。ProtectSystem=strict 會讓整個檔案系統變成唯讀,但核心虛擬檔案系統除外,因此第一次工具呼叫嘗試寫入檔案時,就會以 EROFS: read-only file system 失敗。如果需要這個層級的限制,請在同一次編輯中加入 ReadWritePaths=/var/lib/dsh。
這裡應使用哪個 Type=
Type=exec,因為 dsh 會在前景執行,且永遠不會 fork。相較於預設值,這樣做的好處是能取得實際的錯誤訊息。使用 Type=simple 時,systemd 會在完成 fork 後立即將啟動視為成功,還不知道二進位檔是否存在,因此 systemctl start dsh 會正常返回,而失敗原因只會出現在日誌中。使用 Type=exec 時,systemd 會等待 execve() 成功,因此 ExecStart= 中的拼寫錯誤會直接使你剛輸入的命令失敗,你也能立即看到錯誤。
另外兩個錯誤答案都會卡住。Type=forking 會要求 systemd 等待父程序結束,但 dsh 永遠不會結束,因此啟動程序會持續阻塞,直到 TimeoutStartSec 到期(預設為 90 秒),接著回報 Job for dsh.service failed because a timeout was exceeded.。Type=notify 會等待透過 sd_notify 傳送的 READY=1 訊息,而不會傳送該訊息的 Node 程序也會以相同方式卡住。systemd 服務類型的完整比較涵蓋其餘內容,包括何時值得設定 notify。
明確顯示失敗的重新啟動規則
Restart=on-failure 會在程序以非零狀態結束或收到致命訊號時重新啟動,正常結束後則不再處理該單元。這正是 preview build 所需的行為。如果 dsh 因讀取到不相容的設定而以 0 結束,該單元會停止並保持停止狀態,而 systemctl status dsh 會顯示 inactive (dead),讓您查看相關資訊。Restart=always 會將同一事件轉換成重新啟動迴圈,從遠端看起來像是正常運作。
速率限制是最常被遺漏的部分。systemd 的預設值是在十秒內啟動五次;使用 RestartSec=5s 時,永遠不會在十秒視窗內達到五次,因此啟動時發生當機的單元會無限重新啟動,只有 journal 會記錄這項情況。StartLimitIntervalSec=300 搭配 StartLimitBurst=5 表示五分鐘內失敗五次即已足夠:systemd 會放棄啟動,並將該單元停放在 failed,同時記錄 Start request repeated too quickly.。修正原因後,使用 sudo systemctl reset-failed dsh 清除該狀態。這兩項設定必須放在 [Unit],不能放在 [Service];若放在錯誤的區段,systemd 會靜默忽略它們。
啟動服務,然後檢查
sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dshenable --now 會執行兩項工作。enable 會讓服務在重新開機後恢復執行,--now 則會在本次開機中啟動服務。單獨執行 systemctl start 的效果會在下次重新開機後消失,而核心更新需要重新開機。
systemctl status dsh 應顯示 Active: active (running)、Main PID 和 Memory: 行。接著確認服務正在監聽的位置:
sudo ss -lntp | grep 3080你應該看到 127.0.0.1:3080。如果看到 0.0.0.0:3080,表示某項設定已修改繫結位址,而你的 agent 已暴露在公用網際網路上。該輸出中的程序名稱是 node,不是 dsh,因為 dsh 二進位檔是 Node 指令碼,所以 pgrep -x dsh 找不到任何結果。請改用 systemctl show -p MainPID dsh。
接著重新開機一次。從未成功撐過重新開機的服務,還不能算是真正可用的服務。
sudo reboot重新連線後執行 systemctl is-active dsh。它會輸出 active。
使用 journalctl 讀取日誌
dsh 寫入 stdout 和 stderr 的所有內容,都會以該 unit 名稱寫入 journal。
journalctl -u dsh -f
journalctl -u dsh -n 200 --no-pager
journalctl -u dsh --since "10 min ago" -p err-f 會追蹤新行,-n 會顯示最後 N 筆,-p err 會依優先權篩選。unit 中的 SyslogIdentifier=dsh 會使這些行標記為 dsh,而不是 node。首次讀取未依 unit 篩選的 journal 輸出時,這一點很重要。
請在需要之前,先確認 journal 會在重新開機後保留:
journalctl -u dsh -b -1如果輸出 Specifying boot ID or boot offset has no effect, no persistent journal was found,表示 journal 位於 /run,每次重新開機都會將其丟棄。建立該目錄,然後重新啟動 daemon:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald透過 SSH tunnel 存取 UI,而不是開放公開埠
dsh 會在 127.0.0.1:3080 提供 web UI(使用者介面),不接受在其他位置提供服務。要求 --host 0.0.0.0 時,會顯示以下訊息並停止:
error: --host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead這不是應該設法繞過的限制。web API(應用程式介面)會驅動 agent,而 agent 會執行 shell 命令,因此任何找到該連接埠的人,都能取得 VPS 上的 shell。維護者表示,尚未實作遠端驗證,是將 bind 固定在 loopback 的原因。嘗試變更設定前,建議先閱讀 啟動輸出中的 127.0.0.1:3080 實際代表什麼。請改從自己的電腦轉送該連接埠:
ssh -N -L 3080:127.0.0.1:3080 you@203.0.113.10-L 3080:127.0.0.1:3080 會在筆記型電腦上開啟連接埠 3080,並將該連接埠收到的所有內容傳送至 127.0.0.1:3080;此名稱會在 VPS 上 解析。-N 表示不執行遠端命令,因此工作階段只會維持 tunnel 開啟。請讓它持續執行,並在瀏覽器中開啟 http://127.0.0.1:3080/。您可以在 Settings 再進入 Models,於此輸入 DeepSeek API key,並選擇 workspace 目錄。請將 workspace 指向 /var/lib/dsh/workspace,也就是服務使用者擁有的目錄,否則 agent 的檔案工具會因 EACCES: permission denied 而失效。
如果筆記型電腦上的連接埠 3080 已被使用,ssh 會顯示:
bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080使用 ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10 指定其他本機連接埠,然後瀏覽 http://127.0.0.1:3081/。請在自己的電腦上,將這段輸入儲存到 ~/.ssh/config:
Host dsh-vps
HostName 203.0.113.10
User you
LocalForward 3080 127.0.0.1:3080之後,ssh -N dsh-vps 就是完整的命令。此 tunnel 現在是連往 agent 的唯一入口,因此保護它的關鍵在於 SSH daemon:僅允許金鑰驗證,不允許密碼驗證;其餘 強化 VPS 上的 SSH 安全性 做法也比平常更重要。如果該 VPS 逐漸成為小型私有網路,後方還有資料庫或 staging 主機,透過 使用 subnet router 將這些位址廣告至 tailnet,就不必為每項服務各自設定轉送;不過 dsh 綁定在 loopback,UI 仍須透過 tunnel 存取。
不要將 key 放入 unit file。Environment= 值會由 systemctl show dsh -p Environment 列出,該命令可由該主機上的任何使用者執行。如果安裝的 plugin 需要在環境中提供 key,請將它放在 /etc/dsh.env,設定 mode 600 並由 root 擁有,再以 EnvironmentFile=/etc/dsh.env 引用。systemd 會在執行時以 root 身分讀取該檔案,而 systemctl show 不會輸出其內容。各項設定實際寫入磁碟上的哪個檔案,以及將 dsh 指向本機 Ollama endpoint 而非 DeepSeek API 時,哪些資料會離開您的主機,將在 設定 dsh 的 keys、models 與 endpoints 中說明。
執行成本
推論是在 DeepSeek 的 API 上執行,不是在你的 VPS 上執行。你的主機需要負擔 Node process、提供 UI,以及 agent 決定執行的每個 command。前兩項成本穩定且不高。第三項則沒有受到這個 unit file 的任何限制。
不要直接採用其他人的數據。請在自己的主機上測量最低使用量:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent的單位是 bytes。請在 agent 工作期間監看,不要只在閒置時監看。
Tool calls 是 service 的子程序,因此會位於相同的 control group,並受相同限制約束。在 workspace 中執行 npm install 或測試套件的 agent,使用的 memory 可能大幅超過 harness 本身。在 1 GB VPS 上,問題通常會在此發生:kernel 選擇一個 process 並將其終止,而 journalctl -k | grep -i "out of memory" 會顯示 Out of memory: Killed process 行,指出它選擇的 process。該 process 通常不是最初造成問題的 process。
解決方法是主動設定限制。在 [Service] 區段中加入 MemoryMax= 和 CPUQuota=,即可將損害限制在該 unit 內,讓失控的 build 被終止,而不是讓整台主機鎖死。使用 systemd 限制 memory 和 CPU 說明相關數值與失敗時的行為。Disk 也會持續增加,來源包括 DSH_HOME 下的 session history,以及 agent 寫入 workspace 的內容。因此,請將 du -sh /var/lib/dsh 納入你現有的 disk 監控方式。
如果你需要的是可連線與中斷連線的互動式 agent,service 並不適合這種用途;在持久化的 tmux session 中執行 agent 會更合適。當你需要 dsh 持續執行,並透過 tunnel 存取時,才應將 dsh 作為 unit 執行。
失敗模式與將看到的字串
status=203/EXEC。 systemd 無法執行該檔案,且日誌 Failed to locate executable /usr/local/bin/dsh: No such file or directory。ExecStart= 中的路徑與 command -v dsh 顯示的內容不一致。這是 Type=exec 在 systemctl start 時間回報的失敗,而不是將錯誤隱藏起來。
status=217/USER。 User= 中的帳號不存在。使用 id dsh 確認。
status=200/CHDIR。 WorkingDirectory= 不存在,或服務使用者無法進入該目錄。sudo -u dsh ls /var/lib/dsh/workspace 可直接重現此問題。
Error: listen EADDRINUSE: address already in use 127.0.0.1:3080。 連接埠已被其他程式占用,通常是另一個終端機中的 npx 執行程序仍在使用。sudo ss -lntp | grep 3080 可顯示該程序。
EACCES: permission denied 後接路徑。 /var/lib/dsh 下的擁有權不正確,通常是因為第一次執行時使用了 root,或使用了錯誤的 HOME。sudo chown -R dsh:dsh /var/lib/dsh 可修正。
Start request repeated too quickly.。單元觸發啟動速率限制後停止嘗試。真正的錯誤位於其上方的行。再次嘗試前,先執行 sudo systemctl reset-failed dsh。
單元為 active (running),但瀏覽器沒有顯示內容。 在 VPS 上執行檢查:如果 curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up 在該處顯示 up,表示服務正常,問題出在連接埠轉送。
刻意升級
釘選版本表示升級是由你主動執行,而不是被動發生。先閱讀版本資訊,因為上游針對相容性破壞性變更所發出的警告,正是釘選版本的完整理由。備份狀態目錄,然後替換版本:
sudo systemctl stop dsh
sudo tar czf /root/dsh-home-$(date +%F).tgz -C /var/lib/dsh harness
sudo npm install -g @deepseek-ai/dsh@0.1.0-rc.7
sudo systemctl start dsh
journalctl -u dsh -n 50 --no-pager回復的方式相同,只是將 npm install -g 替換為舊版本,並還原該 tarball;但前提是你先建立了它的備份。預覽階段的 agent runtime 正是可能在升級時改寫底層設定檔格式的軟體。
FAQ
為什麼關閉 SSH 工作階段後,dsh 會停止?
因為 npx @deepseek-ai/dsh web 是由登入工作階段擁有的前景程序,因此工作階段結束時也會被終止。systemd unit 則由 init 系統擁有,所以即使中斷連線,程序仍會繼續執行,重新開機後也會再次啟動。sudo systemctl enable --now dsh 是同時完成這兩項工作的步驟組合:enable 讓服務在重新開機後啟動,--now 讓服務在本次開機時啟動。
dsh 應使用 Type=simple 還是 Type=exec?
Type=exec。dsh 在前景執行,且不會 fork,因此兩者都可運作,但 Type=exec 會讓 systemd 等待 execve() 成功後,才將啟動判定為成功。若 ExecStart= 中的路徑錯誤,systemctl start 會立即失敗,並在你面前顯示 status=203/EXEC。使用 Type=simple 時,同樣的錯誤會回報成功,並隱藏在 journal 中。Type=forking 和 Type=notify 在此處都不正確,而且都會持續等待,直到 TimeoutStartSec 在 90 秒後逾時。
如何從筆記型電腦開啟 dsh Web UI?
透過 SSH 轉送連接埠:ssh -N -L 3080:127.0.0.1:3080 you@your-vps,然後在瀏覽器中開啟 http://127.0.0.1:3080/。不要嘗試將服務繫結至公開位址。dsh 會以 error: --host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead 拒絕 --host 0.0.0.0,因為 Web API 能讓 agent 執行 shell 命令,且前方沒有遠端驗證機制。
可以將 dsh 以 root 身分執行,以簡化權限管理嗎?
不可以。此 harness 用於執行命令及寫入檔案,因此服務具備的權限,agent 也同樣具備。使用 useradd --system --shell /usr/sbin/nologin dsh 建立系統帳號,讓該帳號擁有 /var/lib/dsh,並將 NoNewPrivileges=true 加入 unit。若之後遇到 EACCES: permission denied,通常是因為先前曾以 root 執行,留下由 root 擁有的檔案;sudo chown -R dsh:dsh /var/lib/dsh 可清除此問題。
應在 unit 中固定哪個 dsh 版本?
固定 npm view @deepseek-ai/dsh version 在設定服務時回報的版本,使用 npm install -g @deepseek-ai/dsh@<that version> 安裝,並記錄在你找得到的位置。2026 年 8 月 18 日當時的目前版本是 0.1.0-rc.7。重點不在版本號,而在於不帶版本的 npx 會在啟動時解析套件版本,因此無人值守的重新啟動可能會在沒有明確提示的情況下,改用設定格式不同的版本。