Chạy dsh headless trên VPS bằng systemd
Chạy dsh, DeepSeek Harness, dưới dạng service systemd trên VPS với user riêng, version cố định, Restart, log journalctl và SSH tunnel tới UI.
Chạy dsh không cần terminal trên VPS
Chạy dsh không cần terminal trên VPS chỉ cần một file unit của systemd và một user riêng để sở hữu service đó. dsh là trình khởi chạy dòng lệnh cho DeepSeek Harness, runtime agent của DeepSeek, được phát hành theo giấy phép MIT dưới dạng developer preview vào tháng 8 năm 2026. Quickstart yêu cầu bạn nhập npx @deepseek-ai/dsh web. Cách này là đúng, nhưng tiến trình cũng dừng ngay khi bạn đóng session SSH (secure shell).
Một file unit giải quyết đồng thời 4 vấn đề. Service tự khởi động lại sau reboot. Output được ghi vào journal thay vì cuộn qua terminal. Service chạy bằng một account không phải root. Phiên bản được chạy là phiên bản bạn đã chọn. Điều này đặc biệt quan trọng trong trường hợp này vì upstream ghi rõ:
DeepSeek Harness hiện đang ở giai đoạn developer preview và được cập nhật rất nhanh. SẼ CÓ CÁC THAY ĐỔI LÀM MẤT TƯƠNG THÍCH.
Hướng dẫn này giả định dsh đã chạy được khi bạn khởi động thủ công. Nếu chưa, hãy bắt đầu với cài đặt DeepSeek Harness trên VPS và quay lại sau khi npx @deepseek-ai/dsh web phục vụ được một trang.
Cài Node trước vì npm sẽ không cảnh báo bạn
node -vPackage mặc định của Ubuntu 24.04 là Node 18 (18.19.1 tính đến tháng 8 năm 2026), đã cũ đối với một package được phát hành trong năm nay. @deepseek-ai/dsh không khai báo trường engines, nên npm không in cảnh báo EBADENGINE khi Node của bạn quá cũ. Thay vào đó, lỗi xuất hiện lúc chạy dưới dạng lỗi cú pháp hoặc thiếu module tích hợp sẵn. Đây là thời điểm tệ hơn nhiều để phát hiện lỗi. Hãy cài bản long term support (LTS) hiện tại từ NodeSource:
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 bây giờ phải in ra phiên bản v22. Dòng less có mặt vì việc pipe trực tiếp một script từ xa vào bash sẽ chạy đoạn code mà bạn chưa đọc.
Xác nhận ứng dụng chạy được trước khi viết unit
npx @deepseek-ai/dsh@0.1.0-rc.7 webĐể tiến trình tiếp tục chạy. Mở một phiên SSH thứ hai:
curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo upup cho biết web profile đang lắng nghe trên loopback, là địa chỉ mà ứng dụng bind mặc định. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused cho biết ứng dụng không lắng nghe trên đó, và terminal thứ nhất đang cho biết lý do. Dừng tiến trình chạy thủ công bằng Ctrl+C trước khi tiếp tục: unit cố bind vào một cổng đã bị tiến trình khác chiếm sẽ fail với Error: listen EADDRINUSE: address already in use 127.0.0.1:3080.
0.1.0-rc.7 là version được phát hành vào ngày 18 August 2026. Kiểm tra version hiện tại bằng npm view @deepseek-ai/dsh version, sau đó pin version bạn quyết định chạy.
Cài phiên bản đã pin trên toàn hệ thống
npx là công cụ không phù hợp khi dùng trong file unit. Nó resolve phiên bản package khi process khởi động. Vì vậy, sau 3 tháng, một lần restart có thể chạy một build khác của agent đang ở giai đoạn preview mà bạn không thay đổi gì. Công cụ này cũng cần truy cập npm registry khi boot. Nếu registry chậm, một máy đang hoạt động bình thường có thể khiến unit fail. Hãy cài một lần với phiên bản bạn đã ghi lại:
sudo npm install -g @deepseek-ai/dsh@0.1.0-rc.7
command -v dsh
npm ls -g --depth=0 @deepseek-ai/dshcommand -v dsh in /usr/bin/dsh khi npm được cài từ NodeSource và in /usr/local/bin/dsh khi npm đến từ package riêng của Ubuntu. Dùng đúng path mà lệnh đã in trong file unit. npm ls -g in phiên bản chính xác. Đây là thông tin bạn cần sau 6 tuần, khi hành vi thay đổi và bạn không nhớ mình đã cài phiên bản nào.
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 webSet 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.
Tệp unit
Viết /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= nhận đường dẫn tuyệt đối lấy được từ command -v dsh. systemd sẽ tìm tên command không có đường dẫn trong một danh sách đường dẫn cố định, nhưng danh sách đó không phải PATH của shell. Vì vậy, dùng đường dẫn tuyệt đối sẽ loại bỏ việc phỏng đoán.
WorkingDirectory= là nơi các đường dẫn tương đối được phân giải. Đây cũng là nơi một lần gọi tool chạy ls mà không có đối số sẽ bắt đầu. Trỏ nó đến workspace bạn cung cấp cho agent. Nếu thư mục không tồn tại hoặc service user không thể truy cập thư mục đó, unit sẽ fail với status=200/CHDIR trước khi dsh chạy.
ProtectHome=true ẩn /home và /root khỏi process. Cấu hình này an toàn trong trường hợp này vì mọi thứ service truy cập đều nằm dưới /var/lib/dsh. Nếu trỏ workspace đến một đường dẫn dưới /home, agent sẽ báo thư mục không tồn tại. Điều này dễ gây nhầm lẫn nếu bạn quên dòng này. ProtectSystem=full đặt /usr, /boot và /etc ở chế độ chỉ đọc, trong khi service không cần ghi vào đó.
Mở rộng thêm thường rất hấp dẫn nhưng thường là sai. ProtectSystem=strict làm toàn bộ filesystem ở chế độ chỉ đọc, ngoại trừ các pseudo-filesystem của kernel. Vì vậy, lần gọi tool đầu tiên ghi file sẽ fail với EROFS: read-only file system. Nếu muốn mức hạn chế đó, thêm ReadWritePaths=/var/lib/dsh trong cùng lần chỉnh sửa.
Chọn Type= nào ở đây
Type=exec, vì dsh chạy ở foreground và không fork. Cách này có lợi thế hơn giá trị mặc định là hiển thị lỗi thực tế. Với Type=simple, systemd coi việc start thành công ngay sau khi đã fork, trước khi biết binary có tồn tại hay không, nên systemctl start dsh trả về mà không báo lỗi và lỗi chỉ xuất hiện trong journal. Với Type=exec, systemd chờ execve() thành công, vì vậy lỗi gõ sai trong ExecStart= làm fail chính lệnh bạn vừa nhập, và bạn sẽ thấy lỗi ngay tại đó.
Hai đáp án sai đều bị treo. Type=forking yêu cầu systemd chờ process cha thoát, nhưng dsh không bao giờ thoát, nên lệnh start bị block cho đến khi TimeoutStartSec hết hạn (mặc định là 90 giây), sau đó báo Job for dsh.service failed because a timeout was exceeded. Type=notify chờ một message READY=1 qua sd_notify, nhưng một process Node không bao giờ gửi message đó sẽ bị stall theo cách tương tự. So sánh đầy đủ các loại service của systemd trình bày các phần còn lại, bao gồm cả trường hợp nên đầu tư công sức cấu hình notify.
Quy tắc restart khi có lỗi nghiêm trọng
Restart=on-failure restart khi tiến trình thoát với mã khác 0 hoặc nhận signal nghiêm trọng, nhưng không làm gì thêm sau khi tiến trình thoát bình thường. Đây là hành vi phù hợp với một bản build preview. Nếu dsh thoát với mã 0 vì đọc phải một config không hợp lệ, unit sẽ dừng và giữ nguyên trạng thái dừng. systemctl status dsh hiển thị inactive (dead) để bạn kiểm tra trạng thái này. Restart=always biến cùng sự kiện đó thành một vòng lặp restart nhưng nhìn từ bên ngoài vẫn có vẻ đang hoạt động bình thường.
Rate limit là phần thường bị bỏ qua. Mặc định của systemd là 5 lần start trong 10 giây. Với RestartSec=5s, unit sẽ không bao giờ đạt 5 lần start trong một cửa sổ 10 giây. Vì vậy, unit bị crash khi khởi động sẽ restart vô hạn và chỉ journal mới ghi nhận được việc này. StartLimitIntervalSec=300 cùng với StartLimitBurst=5 có nghĩa là 5 lần fail trong 5 phút là đủ để systemd từ bỏ. systemd sẽ dừng unit tại failed và ghi log Start request repeated too quickly.. Sau khi sửa nguyên nhân, dùng sudo systemctl reset-failed dsh để xóa trạng thái đó. Cả hai thiết lập phải nằm trong [Unit], không phải [Service]. systemd sẽ âm thầm bỏ qua chúng nếu đặt trong section sai.
Khởi động service, rồi kiểm tra
sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dshenable --now thực hiện 2 việc. enable giúp service khởi động lại sau reboot, còn --now khởi động service trong lần boot hiện tại. Chạy riêng systemctl start sẽ mất hiệu lực sau lần reboot tiếp theo, mà các bản cập nhật kernel thường yêu cầu reboot.
systemctl status dsh phải hiển thị Active: active (running), một dòng Main PID và một dòng Memory:. Sau đó xác nhận service đang listen ở đâu:
sudo ss -lntp | grep 3080Bạn cần thấy 127.0.0.1:3080. Nếu thấy 0.0.0.0:3080, địa chỉ bind đã bị thay đổi và agent đang public trên Internet. Tên process trong output đó là node, không phải dsh, vì binary dsh là một Node script nên pgrep -x dsh không tìm thấy gì. Thay vào đó, dùng systemctl show -p MainPID dsh.
Sau đó reboot một lần. Một service chưa từng sống sót qua reboot thì chưa thể xem là service thực sự.
sudo rebootKết nối lại và chạy systemctl is-active dsh. Lệnh này in ra active.
Đọc log bằng journalctl
Mọi thứ dsh ghi vào stdout và stderr đều được đưa vào journal dưới tên unit.
journalctl -u dsh -f
journalctl -u dsh -n 200 --no-pager
journalctl -u dsh --since "10 min ago" -p err-f theo dõi các dòng mới, -n hiển thị N dòng cuối, còn -p err lọc theo priority. SyslogIdentifier=dsh trong unit khiến các dòng đó được gắn nhãn dsh thay vì node. Điều này quan trọng khi bạn đọc journal output lần đầu mà không lọc theo unit.
Hãy kiểm tra journal có được giữ lại sau reboot trước khi bạn cần dùng:
journalctl -u dsh -b -1Nếu lệnh in ra Specifying boot ID or boot offset has no effect, no persistent journal was found, journal đang nằm trong /run và sẽ bị xóa sau mỗi lần reboot. Tạo thư mục rồi restart daemon:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journaldTruy cập UI qua SSH tunnel, không mở port public
dsh cung cấp web UI (user interface) trên 127.0.0.1:3080 và từ chối cung cấp ở bất kỳ nơi nào khác. Nếu yêu cầu --host 0.0.0.0, nó sẽ dừng với lỗi:
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Đây không phải là hạn chế cần tìm cách vượt qua. Web API (application programming interface) điều khiển agent, còn agent chạy shell command. Vì vậy, một port có thể truy cập được cũng đồng nghĩa với một shell trên VPS của bạn đối với bất kỳ ai tìm thấy port đó. Maintainer cho biết lý do bind cố định vào loopback là remote authentication chưa được triển khai. Hãy forward port từ máy của bạn:
ssh -N -L 3080:127.0.0.1:3080 you@203.0.113.10-L 3080:127.0.0.1:3080 mở port 3080 trên laptop của bạn và gửi mọi dữ liệu đến đó tới 127.0.0.1:3080, với địa chỉ được phân giải trên VPS. -N nghĩa là không chạy remote command, nên session chỉ giữ tunnel hoạt động. Hãy để lệnh này chạy và mở http://127.0.0.1:3080/ trong browser. Đây là nơi bạn nhập DeepSeek API key, tại Settings rồi Models, và chọn workspace directory. Trỏ workspace vào /var/lib/dsh/workspace, directory do service user sở hữu; nếu không, các file tool của agent sẽ lỗi với EACCES: permission denied.
Nếu port 3080 đang được sử dụng trên laptop, ssh sẽ báo:
bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080Chọn port local khác bằng ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10, rồi truy cập http://127.0.0.1:3081/. Lưu lệnh này vào ~/.ssh/config trên máy của bạn:
Host dsh-vps
HostName 203.0.113.10
User you
LocalForward 3080 127.0.0.1:3080Sau đó, ssh -N dsh-vps là toàn bộ command cần dùng. Tunnel này hiện là lối duy nhất để truy cập agent, nên SSH daemon là thành phần bảo vệ agent: chỉ dùng key, không dùng password authentication, và các bước hardening SSH trên VPS của bạn càng quan trọng hơn trong trường hợp này.
Không đặt key trong unit file. Các giá trị Environment= được systemctl show dsh -p Environment in ra; bất kỳ user nào trên máy cũng có thể chạy lệnh này. Nếu plugin bạn cài cần key trong environment, hãy đặt key trong /etc/dsh.env với mode 600, thuộc sở hữu của root, rồi tham chiếu file bằng EnvironmentFile=/etc/dsh.env. systemd đọc file đó với quyền root tại thời điểm exec, còn systemctl show không in nội dung của file.
Chi phí vận hành
Inference diễn ra qua API của DeepSeek, không diễn ra trên VPS của bạn. Máy chủ chỉ phải chạy tiến trình Node, UI mà nó cung cấp và mọi command mà agent quyết định chạy. Hai phần đầu ổn định và nhỏ. Phần thứ ba không bị giới hạn trong unit file này.
Tự đo mức sử dụng tối thiểu trên máy của bạn thay vì dùng số liệu từ máy của người khác:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent tính theo bytes. Hãy theo dõi giá trị này khi agent đang làm việc, không phải khi nó ở trạng thái idle.
Các tool call là tiến trình con của service, nên chúng nằm trong cùng control group và dùng chung các giới hạn. Agent chạy npm install hoặc một test suite trong workspace có thể dùng nhiều memory hơn chính harness với mức chênh lệch lớn. Trên VPS 1 GB, đây là lúc sự cố xảy ra: kernel chọn một tiến trình và kill nó, còn journalctl -k | grep -i "out of memory" hiển thị dòng Out of memory: Killed process cho biết tiến trình được chọn. Tiến trình đó thường không phải tiến trình gây ra vấn đề.
Cách xử lý là đặt giới hạn một cách có chủ đích. MemoryMax= và CPUQuota= trong phần [Service] giữ tác động trong phạm vi unit, để một build chạy mất kiểm soát bị kill thay vì làm toàn bộ máy bị treo. Giới hạn memory và CPU bằng systemd trình bày các giá trị giới hạn và cách hệ thống xử lý khi có lỗi. Disk cũng tăng do lịch sử session trong DSH_HOME và do mọi dữ liệu agent ghi vào workspace, vì vậy hãy đưa du -sh /var/lib/dsh vào công cụ bạn đang dùng để theo dõi disk.
Nếu bạn cần một agent tương tác mà có thể attach và detach, service không phải mô hình phù hợp; chạy agent trong session tmux liên tục sẽ phù hợp hơn. Hãy chạy dsh dưới dạng unit khi bạn muốn nó luôn hoạt động và có thể truy cập qua tunnel.
Các trạng thái lỗi và chuỗi bạn sẽ thấy
status=203/EXEC. systemd không thể chạy file và ghi log Failed to locate executable /usr/local/bin/dsh: No such file or directory. Đường dẫn trong ExecStart= không khớp với kết quả command -v dsh đã in ra. Đây là lỗi mà Type=exec báo tại thời điểm systemctl start thay vì che giấu.
status=217/USER. Account trong User= không tồn tại. Xác nhận bằng id dsh.
status=200/CHDIR. WorkingDirectory= không tồn tại hoặc service user không thể truy cập vào đó. sudo -u dsh ls /var/lib/dsh/workspace tái hiện trực tiếp lỗi này.
Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. Một tiến trình khác đang giữ port, thường là tiến trình npx chạy trước đó vẫn còn mở trong một terminal khác. sudo ss -lntp | grep 3080 cho biết process đó.
EACCES: permission denied kèm theo một đường dẫn. Ownership bên dưới /var/lib/dsh không đúng, thường do lần chạy đầu tiên được thực hiện bằng root hoặc với HOME không đúng. sudo chown -R dsh:dsh /var/lib/dsh sẽ sửa lỗi này.
Start request repeated too quickly. Unit đã chạm giới hạn start rate và dừng thử lại. Lỗi thực sự nằm ở các dòng phía trên. Chạy sudo systemctl reset-failed dsh trước khi thử lại.
Unit là active (running) nhưng trình duyệt không hiển thị gì. Chạy lệnh kiểm tra trên VPS: nếu curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up in ra up tại đó thì service vẫn hoạt động bình thường và vấn đề nằm ở port forward.
Nâng cấp có chủ đích
Pin version nghĩa là bạn chủ động thực hiện nâng cấp, thay vì để hệ thống tự động nâng cấp. Đọc release note trước, vì cảnh báo của upstream về các thay đổi có thể phá vỡ compatibility chính là lý do cần pin version. Sao lưu thư mục lưu state, rồi đổi sang version mới:
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-pagerRollback cũng thực hiện npm install -g với version cũ, sau đó khôi phục tarball đó. Cách này chỉ hoạt động nếu bạn đã tạo tarball trước đó. Preview-stage agent runtime chính là loại phần mềm có thể ghi lại format của config trong quá trình nâng cấp.
FAQ
Vì sao dsh dừng khi tôi đóng phiên SSH?
Vì npx @deepseek-ai/dsh web là tiến trình foreground thuộc phiên đăng nhập của bạn, nên nó bị dừng khi phiên kết thúc. Một systemd unit lại thuộc init system, nên vẫn chạy sau khi bạn disconnect và khởi động lại sau reboot. sudo systemctl enable --now dsh là cặp bước giúp bạn có cả hai: enable cho reboot và --now cho lần boot này.
Tôi nên dùng Type=simple hay Type=exec cho dsh?
Type=exec. dsh chạy ở foreground và không bao giờ fork, nên cả hai đều hoạt động, nhưng Type=exec khiến systemd chờ execve() chạy thành công trước khi báo start thành công. Khi đó, đường dẫn sai trong ExecStart= sẽ làm systemctl start fail với status=203/EXEC hiển thị ngay trước mắt bạn. Với Type=simple, cùng lỗi đó vẫn trả về success và bị ẩn trong journal. Type=forking và Type=notify đều không phù hợp ở đây, và cả hai đều treo cho đến khi TimeoutStartSec hết hạn sau 90 giây.
Làm cách nào để mở web UI của dsh từ laptop?
Forward cổng qua SSH: ssh -N -L 3080:127.0.0.1:3080 you@your-vps, sau đó mở http://127.0.0.1:3080/ trong browser. Không bind service vào địa chỉ public. dsh từ chối --host 0.0.0.0 với 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, vì web API có thể yêu cầu agent chạy shell command và không có cơ chế remote authentication nào đứng trước nó.
Tôi có thể chạy dsh với root để đơn giản hóa quyền không?
Không. Harness này dùng để chạy command và ghi file, nên service có quyền gì thì agent cũng có các quyền đó. Tạo system account bằng useradd --system --shell /usr/sbin/nologin dsh, đặt account đó làm owner của /var/lib/dsh, rồi thêm NoNewPrivileges=true vào unit. Nếu sau đó gặp EACCES: permission denied, nguyên nhân thường là một lần chạy trước đó với root đã để lại các file thuộc root; sudo chown -R dsh:dsh /var/lib/dsh sẽ xóa chúng.
Tôi nên pin phiên bản dsh nào trong unit?
Dùng phiên bản mà npm view @deepseek-ai/dsh version báo khi bạn thiết lập service, cài bằng npm install -g @deepseek-ai/dsh@<that version> và ghi lại ở nơi bạn có thể tìm thấy. 0.1.0-rc.7 là phiên bản hiện tại vào ngày 18 August 2026. Vấn đề không nằm ở con số, mà ở chỗ npx không có version sẽ resolve package tại thời điểm start, nên một lần restart không cần giám sát có thể âm thầm chuyển bạn sang build có config format khác.