SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-23

如何在 VPS 上以 systemd 背景執行 dsh

在 VPS 上以 systemd 執行 dsh:建立專用帳號、固定版本與 Restart 規則,使用 journalctl 查閱日誌,並透過 SSH tunnel 存取 UI。

在 VPS 上以無終端機模式執行 dsh

在 VPS 上以無終端機模式執行 dsh,只需要一個 systemd unit file,以及一個專用帳號來擁有該服務。dsh 是 DeepSeek Harness 的命令列啟動程式。DeepSeek Harness 是 DeepSeek 的代理程式執行環境,於 2026 年 8 月以 developer preview 形式,依 MIT 授權條款發布。快速入門指南要求您輸入 npx @deepseek-ai/dsh web,這個做法是正確的,但您一旦關閉 SSH(secure shell)工作階段,程序也會立即終止。

unit file 可同時解決四個問題。服務會在重新開機後恢復。輸出會寫入 journal,不會在終端機中快速捲過。服務會以非 root 帳號執行。服務執行的版本也會是您選定的版本。這一點在此特別重要,因為上游以大寫字母明確警告:

DeepSeek Harness 目前處於 developer preview 階段,並且持續快速迭代。THERE WILL BE COMPATIBILITY-BREAKING CHANGES.

本指南假設 dsh 已可由您手動執行。如果尚未可用,請先參閱 在 VPS 上安裝 DeepSeek Harness,待 npx @deepseek-ai/dsh web 能提供頁面後再返回本指南。

先確認 Node,因為 npm 不會警告你

node -v

Ubuntu 24.04 隨附的套件是 Node 18(截至 2026 年 8 月為 18.19.1)。對今年發布的套件而言,這個版本已經過舊。@deepseek-ai/dsh 沒有發布 engines 欄位,因此 Node 版本過舊時,npm 不會顯示 EBADENGINE 警告。問題會改在執行階段才出現,通常是語法錯誤或缺少內建功能。等到這時才發現,排查位置會更不理想。請從 NodeSource 安裝目前的 long term support (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 -v

node -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 up

up 表示 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 檢查目前版本,再釘選你決定執行的版本。

安裝固定的版本並設為全域套件

npx 不適合在 unit file 中使用。它會在程序啟動時解析套件版本,因此 3 個月後重新啟動時,預覽階段 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。

A user that owns the service and nothing else

The agent runs shell commands. That is its job. Running it as root makes every tool call a root tool call, so give it its own account with no login 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 becomes DSH_HOME, the directory dsh keeps profiles in. A profile is a named stack of plugin bundles with your own patch layer on top, and the web and headless profiles build themselves from shipped templates the first time you boot them. That first boot writes files and may fetch bundles, so do it by hand where you can watch it.

sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile web

Set HOME explicitly rather than trusting what sudo does with it, because whether sudo rewrites HOME for a non-login command depends on the set_home setting in /etc/sudoers. Get it wrong and the first run drops cache directories into your home directory owned by dsh, and the service later cannot find its own state. Stop it with Ctrl+C once the curl check returns up.

unit 檔案

撰寫 /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.target

ExecStart= 需要使用從 command -v dsh 取得的絕對路徑。systemd 會針對未附路徑的命令名稱搜尋固定的路徑清單,但該清單不是 shell 的 PATH。因此,使用絕對路徑即可避免猜測。

WorkingDirectory= 是相對路徑解析的位置,也是未帶引數執行 ls 的工具呼叫起始位置。請將它指向提供給 agent 的工作區。如果目錄不存在,或服務使用者無法進入該目錄,unit 會在 dsh 完全啟動前因 status=200/CHDIR 而失敗。

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 會在前景執行,且從不建立子程序。相較於預設值,這樣做的好處是能取得實際的錯誤訊息。使用 Type=simple 時,systemd 會在完成 fork 後立即判定啟動成功,甚至還不知道該二進位檔是否存在,因此 systemctl start dsh 會正常返回,錯誤只會出現在 journal 中。使用 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 會在程序以非零狀態碼結束或收到致命訊號時重新啟動,正常結束後則不會再次啟動單元。這正是預覽版本所需的行為。如果 dsh 因讀取不相容的設定而以 0 結束,單元會停止並維持停止狀態,而 systemctl status dsh 會在 inactive (dead) 顯示相關資訊。Restart=always 會將相同事件轉成重新啟動迴圈,從遠端看起來像是正常運作。

速率限制是最常被遺漏的部分。systemd 的預設值是在十秒內最多啟動五次;使用 RestartSec=5s 時,永遠不會在十秒視窗內達到五次,因此啟動時持續當機的單元會無限重新啟動,只有日誌能顯示這個問題。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 dsh

enable --now會執行兩項工作。enable會讓服務在重新開機後恢復執行,而--now會在本次開機中啟動服務。單獨執行systemctl start的效果會在下次重新開機後消失,而核心更新會要求重新開機。

systemctl status dsh應顯示Active: active (running)Main PIDMemory:行。接著確認服務正在監聽的位置:

sudo ss -lntp | grep 3080

你應看到127.0.0.1:3080。如果看到0.0.0.0:3080,表示某項設定已修改繫結位址,代理程式目前暴露在公用網際網路上。該輸出中的程序名稱是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。請改用自己的電腦轉送該埠:

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 的做法也都適用,而且重要性更高。

不要將 key 放入 unit file。Environment= 值會由 systemctl show dsh -p Environment 顯示,任何登入該主機的使用者都能執行此命令。如果安裝的 plugin 需要環境中的 key,請將它放在 /etc/dsh.env,設定 mode 600 並由 root 擁有,再透過 EnvironmentFile=/etc/dsh.env 引用。systemd 會在 exec 時以 root 身分讀取該檔案,而 systemctl show 不會顯示其內容。各項設定實際寫入磁碟上的哪個檔案,以及將 dsh 指向本機 Ollama endpoint 而非 DeepSeek API 時,哪些資料會離開主機,將在設定 dsh 的 keys、models 與 endpoints中說明。

執行成本

推論是在 DeepSeek 的 API 上執行,不是在你的 VPS 上執行。你的伺服器需要負擔 Node process、它提供的 UI,以及 agent 決定執行的每個命令。前兩項穩定且負載很小。第三項則沒有受到這個 unit file 的任何限制。

不要直接採用他人伺服器的數據。請在自己的伺服器上測量最低負載:

systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2

MemoryCurrent 的單位是 bytes。請在 agent 工作期間監看,不要只在閒置時監看。

Tool call 是該服務的子程序,因此會進入相同的 control group,並計入相同的限制。agent 若在 workspace 中執行 npm install 或測試套件,使用的記憶體可能大幅超過 harness 本身。在 1 GB VPS 上,問題通常會從這裡開始:kernel 選取一個 process 並將其終止,而 journalctl -k | grep -i "out of memory" 會顯示 Out of memory: Killed process 行,列出它選取的 process。該 process 通常不是造成問題的 process。

解決方法是主動設定限制。MemoryMax=CPUQuota= 放在 [Service] 區段中,可將影響限制在該 unit 內。如此一來,失控的 build 會被終止,而不是讓整台伺服器停止回應。使用 systemd 限制記憶體與 CPU 說明相關數值與失效行為。磁碟使用量也會增加,來源包括 DSH_HOME 下的 session history,以及 agent 寫入 workspace 的內容。因此,請將 du -sh /var/lib/dsh 加入你現有的磁碟監控機制。

如果你需要的是可連線並可中斷連線的互動式 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 directoryExecStart= 中的路徑與 command -v dsh 顯示的內容不一致。這就是 Type=execsystemctl 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,或使用了錯誤的 HOMEsudo 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;但這只有在你事先建立 tarball 時才可行。預覽階段的 agent runtime 正是升級可能直接改寫設定格式的軟體。

FAQ

為什麼關閉 SSH 工作階段後,dsh 就會停止?

因為 npx @deepseek-ai/dsh web 是由登入工作階段擁有的前景程序,因此工作階段結束時也會一併終止。systemd unit 則由 init system 擁有,所以即使中斷連線仍會繼續執行,重新開機後也會再次啟動。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=forkingType=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 命令,而其前方沒有遠端驗證機制。

可以以 root 執行 dsh,讓權限管理更簡單嗎?

不可以。此 harness 用來執行命令與寫入檔案,因此服務擁有的權限也會由 agent 擁有。使用 useradd --system --shell /usr/sbin/nologin dsh 建立 system account,讓它擁有 /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> 安裝,並記錄在日後找得到的位置。0.1.0-rc.7 在 18 August 2026 當時是最新版本。重點不在版本號,而在於不指定版本的 npx 會在啟動時解析套件,因此無人值守的重新啟動可能會悄悄改用設定格式不同的 build。