วิธีรัน dsh แบบ Headless บน VPS ด้วย systemd
เรียนรู้วิธีตั้งค่า dsh ให้ทำงานเบื้องหลังบน VPS ด้วย systemd เพื่อจัดการบริการอัตโนมัติ รองรับการรีสตาร์ทอัตโนมัติ การเก็บ Log ผ่าน journalctl และการกำหนดสิทธิ์ผู้ใช้
การรัน dsh แบบ headless บน VPS โดยไม่ผ่าน terminal
การรัน dsh แบบ headless บน VPS ทำได้โดยการสร้างไฟล์ systemd unit และกำหนดผู้ใช้เฉพาะสำหรับรันโปรแกรม dsh คือ command line launcher สำหรับ DeepSeek Harness ซึ่งเป็น agent runtime ของ DeepSeek ที่เผยแพร่ภายใต้สัญญาอนุญาต MIT ในช่วง developer preview เมื่อเดือนสิงหาคม 2026 Harness คือโปรแกรมที่ครอบตัวโมเดลไว้ ไม่ใช่ตัวโมเดลเอง ดังนั้นสิ่งที่คุณกำลังนำไปรันภายใต้ systemd คือ ลูป, เครื่องมือ และสิทธิ์การเข้าถึง ไม่ใช่การประมวลผล inference ของ DeepSeek คู่มือเริ่มต้นใช้งานแนะนำให้คุณพิมพ์ npx @deepseek-ai/dsh web ซึ่งเป็นวิธีที่ถูกต้อง แต่โปรแกรมจะหยุดทำงานทันทีที่คุณปิดเซสชัน SSH (secure shell)
ไฟล์ unit จะช่วยแก้ไขปัญหา 4 ประการในคราวเดียว คือ บริการจะกลับมาทำงานใหม่หลังจากรีบูต, เอาต์พุตจะถูกส่งไปยัง journal แทนที่จะเลื่อนผ่านหน้าจอคุณไป, รันในฐานะบัญชีผู้ใช้ที่ไม่ใช่ root และรันเวอร์ชันที่คุณเลือกไว้ ซึ่งมีความสำคัญมากกว่าปกติเนื่องจากผู้พัฒนาต้นทางได้ระบุไว้ด้วยตัวพิมพ์ใหญ่ว่า:
DeepSeek Harness อยู่ในสถานะ developer preview และมีการพัฒนาอย่างรวดเร็ว จะมีการเปลี่ยนแปลงที่ทำให้ไม่สามารถใช้งานร่วมกับเวอร์ชันก่อนหน้าได้
คู่มือนี้อนุมานว่าคุณสามารถใช้งาน dsh ด้วยตนเองได้แล้ว หากยังใช้งานไม่ได้ ให้เริ่มต้นด้วย การติดตั้ง DeepSeek Harness บน VPS และกลับมาดำเนินการต่อเมื่อ npx @deepseek-ai/dsh web สามารถแสดงหน้าเว็บได้แล้ว
ติดตั้ง Node ก่อน เพราะ npm จะไม่แจ้งเตือนคุณ
node -vแพ็กเกจ Node 18 (เวอร์ชัน 18.19.1 ณ เดือนสิงหาคม 2026) ที่มากับ Ubuntu 24.04 นั้นถือว่าเก่าเกินไปสำหรับแพ็กเกจที่เผยแพร่ในปีนี้ เนื่องจาก @deepseek-ai/dsh ไม่ได้ระบุฟิลด์ engines ไว้ npm จึงไม่แสดงคำเตือน EBADENGINE เมื่อเวอร์ชันของ Node ที่คุณใช้เก่าเกินไป ความล้มเหลวจะไปเกิดขึ้นในขณะรันไทม์แทนในรูปแบบของ syntax error หรือฟังก์ชัน built-in ที่หายไป ซึ่งเป็นจุดที่ตรวจสอบปัญหาได้ยากกว่ามาก ให้ติดตั้งเวอร์ชัน long term support (LTS) ปัจจุบันจาก 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 -vตอนนี้ node -v ควรแสดงเวอร์ชัน v22 ออกมา บรรทัด less ปรากฏขึ้นเนื่องจากการส่งสคริปต์จากระยะไกลผ่าน pipe เข้าสู่ bash โดยตรงนั้นเป็นการรันโค้ดที่คุณไม่ได้ตรวจสอบก่อนใช้งาน
พิสูจน์ว่าโปรแกรมทำงานได้ก่อนเขียน unit
npx @deepseek-ai/dsh@0.1.0-rc.7 webปล่อยให้โปรแกรมทำงานค้างไว้ จากนั้นเปิด SSH session ที่สองขึ้นมา:
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 หมายความว่าไม่ใช่ และ terminal แรกจะแจ้งสาเหตุให้คุณทราบ ให้หยุดการรันด้วยตนเองด้วย Ctrl+C ก่อนดำเนินการต่อ เพราะ unit ที่พยายามผูกพอร์ตซึ่งถูกใช้งานอยู่แล้วจะล้มเหลวด้วย Error: listen EADDRINUSE: address already in use 127.0.0.1:3080
0.1.0-rc.7 คือเวอร์ชันที่เผยแพร่เมื่อวันที่ 18 สิงหาคม 2026 ให้ตรวจสอบเวอร์ชันปัจจุบันด้วย npm view @deepseek-ai/dsh version จากนั้นจึงระบุเวอร์ชันที่คุณต้องการใช้งานให้ชัดเจน (pin)
ติดตั้งเวอร์ชันที่คุณระบุไว้แบบ global
npx ไม่ใช่เครื่องมือที่เหมาะสมสำหรับการใช้งานภายใน unit file เนื่องจากคำสั่งนี้จะทำการตรวจสอบเวอร์ชันของแพ็กเกจในขณะที่โปรเซสเริ่มทำงาน ซึ่งหมายความว่าหากมีการรีสตาร์ทเครื่องในอีก 3 เดือนข้างหน้า ระบบอาจดึงเอา build ใหม่ของ agent ที่ยังอยู่ในขั้นทดสอบมาใช้งานโดยที่คุณไม่ได้ตั้งใจ นอกจากนี้ยังจำเป็นต้องเชื่อมต่อกับ npm registry ได้ในขณะบูตเครื่อง ซึ่งจะทำให้เครื่องที่เคยทำงานได้ปกติกลายเป็น unit ที่ล้มเหลวในวันที่ registry ทำงานช้า ให้ทำการติดตั้งเพียงครั้งเดียวด้วยเวอร์ชันที่คุณระบุไว้ชัดเจน:
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 จะแสดงผลเป็น /usr/bin/dsh หาก npm มาจาก NodeSource และแสดงเป็น /usr/local/bin/dsh หากมาจากแพ็กเกจของ Ubuntu เอง ให้ใช้ path ที่แสดงผลจริงใน unit file ส่วน npm ls -g จะแสดงเวอร์ชันที่ติดตั้งอยู่จริง ซึ่งเป็นข้อมูลที่คุณจำเป็นต้องใช้ในอีก 6 สัปดาห์ข้างหน้าเมื่อพฤติกรรมการทำงานของซอฟต์แวร์เปลี่ยนไปและคุณจำไม่ได้ว่าได้ติดตั้งเวอร์ชันใดไว้ หากการติดตั้งล้มเหลว หรือ command -v dsh ไม่แสดงผลลัพธ์ใดๆ หลังจากนั้น หรือเวอร์ชันที่ได้รับไม่ใช่เวอร์ชันที่คุณต้องการ ให้ตรวจสอบขั้นตอนใน ปัญหาทั่วไปในการติดตั้งและการตรวจสอบเวอร์ชันด้วย dsh ก่อนที่จะเขียน unit file
ผู้ใช้ที่เป็นเจ้าของบริการและไม่มีสิทธิ์อื่น
เอเจนต์ทำหน้าที่รันคำสั่งเชลล์ ซึ่งเป็นงานหลักของมัน การรันในฐานะ root จะทำให้ทุกคำสั่งที่เรียกใช้มีสิทธิ์เป็น root ทั้งหมด ดังนั้นควรสร้างบัญชีผู้ใช้แยกต่างหากสำหรับเอเจนต์โดยเฉพาะและกำหนดให้ไม่มีเชลล์สำหรับล็อกอิน
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 ใช้เก็บโปรไฟล์ โปรไฟล์คือชุดของปลั๊กอินที่รวมกันเป็น stack โดยมีเลเยอร์ patch ของคุณวางทับอยู่ด้านบน และโปรไฟล์ web กับ headless จะสร้างตัวเองขึ้นจากเทมเพลตที่มาพร้อมกับซอฟต์แวร์ในการบูตครั้งแรก สิ่งใดก็ตามที่คุณเพิ่มเข้าไปใน stack นั้นในภายหลังจะรันในฐานะผู้ใช้นี้โดยใช้สิทธิ์ไฟล์และเชลล์ของเอเจนต์เอง ดังนั้น การตรวจสอบปลั๊กอินก่อนติดตั้ง จึงเป็นงานส่วนเดียวกับการสร้างบัญชีผู้ใช้ การบูตครั้งแรกจะมีการเขียนไฟล์และอาจมีการดึงข้อมูลชุดปลั๊กอิน ดังนั้นควรดำเนินการด้วยตนเองในขั้นตอนที่คุณสามารถตรวจสอบได้
sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile webกำหนดค่า HOME ให้ชัดเจนแทนที่จะเชื่อใจสิ่งที่ sudo จัดการให้ เพราะการที่ sudo จะเขียนทับ HOME สำหรับคำสั่งที่ไม่ใช่การล็อกอินหรือไม่นั้น ขึ้นอยู่กับการตั้งค่า set_home ใน /etc/sudoers หากตั้งค่าผิดพลาด การรันครั้งแรกจะทิ้งไดเรกทอรีแคชไว้ในโฮมไดเรกทอรี ของคุณ ซึ่งมีเจ้าของเป็น dsh และจะทำให้บริการไม่สามารถค้นหาสถานะของตัวเองได้ในภายหลัง ให้หยุดการทำงานด้วย Ctrl+C ทันทีที่การตรวจสอบ curl ส่งค่ากลับเป็น 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.targetExecStart= ใช้พาธแบบสัมบูรณ์ (absolute path) ที่คุณได้มาจาก command -v dsh โดย systemd จะค้นหาชื่อคำสั่งที่ระบุลอยๆ จากรายการพาธที่กำหนดไว้ตายตัว ซึ่งไม่ใช่ PATH ของ shell คุณ ดังนั้นการใช้พาธแบบสัมบูรณ์จึงช่วยลดความผิดพลาดในการคาดเดา
WorkingDirectory= คือตำแหน่งที่พาธแบบสัมพัทธ์ (relative path) จะถูกแปลงเป็นพาธจริง และเป็นจุดเริ่มต้นเมื่อมีการเรียกใช้เครื่องมือที่รัน ls โดยไม่มีอาร์กิวเมนต์ ให้ชี้ค่านี้ไปยัง workspace ที่คุณกำหนดให้ agent หากไดเรกทอรีดังกล่าวไม่มีอยู่จริงหรือผู้ใช้ที่รัน service ไม่สามารถเข้าถึงได้ unit จะล้มเหลวด้วยสถานะ status=200/CHDIR ก่อนที่ dsh จะเริ่มทำงาน
ProtectHome=true จะซ่อน /home และ /root ไม่ให้กระบวนการมองเห็น ซึ่งปลอดภัยในกรณีนี้เนื่องจากทุกสิ่งที่ service เข้าถึงนั้นอยู่ภายใต้ /var/lib/dsh หากคุณชี้ workspace ไปยังพาธภายใต้ /home ตัว agent จะรายงานว่าไดเรกทอรีไม่มีอยู่จริง ซึ่งอาจทำให้สับสนได้หากคุณลืมบรรทัดนี้ ส่วน ProtectSystem=full จะทำให้ /usr, /boot และ /etc เป็นแบบอ่านได้อย่างเดียว (read-only) ซึ่งเป็นสิ่งที่ service ไม่จำเป็นต้องเขียนข้อมูลลงไป
การตั้งค่าเพิ่มเติมอาจดูน่าสนใจแต่โดยปกติแล้วมักจะไม่ถูกต้อง ProtectSystem=strict จะทำให้ระบบไฟล์ทั้งหมดเป็นแบบอ่านได้อย่างเดียว ยกเว้น pseudo-filesystem ของ kernel ดังนั้นการเรียกใช้เครื่องมือใดๆ ที่มีการเขียนไฟล์จะล้มเหลวด้วยข้อผิดพลาด EROFS: read-only file system หากคุณต้องการความปลอดภัยในระดับนั้น ให้เพิ่ม ReadWritePaths=/var/lib/dsh เข้าไปในการแก้ไขเดียวกัน
ควรใช้ Type= แบบใดที่นี่
Type=exec เพราะ dsh ทำงานใน foreground และไม่มีการ fork กระบวนการ สิ่งที่คุณได้รับมากกว่าค่าเริ่มต้นคือข้อความแสดงข้อผิดพลาดที่แท้จริง หากใช้ Type=simple ระบบ systemd จะถือว่าการเริ่มทำงานสำเร็จทันทีที่กระบวนการ fork เสร็จสิ้น ก่อนที่จะทราบด้วยซ้ำว่า binary นั้นมีอยู่จริงหรือไม่ ดังนั้น systemctl start dsh จึงส่งค่ากลับอย่างปกติ และความล้มเหลวจะปรากฏให้เห็นเฉพาะใน journal เท่านั้น แต่ด้วย Type=exec ระบบ systemd จะรอให้ execve() ทำงานสำเร็จ ดังนั้นหากพิมพ์ ExecStart= ผิดพลาด คำสั่งที่คุณเพิ่งพิมพ์ไปจะล้มเหลว ซึ่งคุณจะเห็นข้อผิดพลาดนั้นทันที
คำตอบที่ผิดทั้งสองแบบจะทำให้ระบบค้าง Type=forking สั่งให้ systemd รอจนกว่ากระบวนการหลัก (parent process) จะจบการทำงาน แต่ dsh ไม่เคยจบการทำงาน ดังนั้นการเริ่ม service จึงค้างจนกว่า TimeoutStartSec จะหมดเวลา (ค่าเริ่มต้นคือ 90 วินาที) แล้วจึงรายงานข้อผิดพลาด Job for dsh.service failed because a timeout was exceeded. ส่วน Type=notify จะรอข้อความ READY=1 ผ่านทาง sd_notify ซึ่งหากกระบวนการ Node ไม่เคยส่งข้อความดังกล่าวออกมา ระบบก็จะค้างในลักษณะเดียวกัน การเปรียบเทียบประเภทของ systemd service อย่างละเอียด ครอบคลุมเนื้อหาส่วนที่เหลือ รวมถึงกรณีที่การตั้งค่า notify นั้นคุ้มค่าที่จะทำ
กฎการรีสตาร์ทที่แจ้งเตือนเมื่อเกิดความล้มเหลว
Restart=on-failure จะทำการรีสตาร์ทเมื่อได้รับ exit code ที่ไม่ใช่ศูนย์หรือได้รับสัญญาณ fatal signal และจะปล่อยให้ unit ทำงานต่อไปหลังจาก exit อย่างปกติ นี่คือพฤติกรรมที่คุณต้องการสำหรับ preview build หาก dsh จบการทำงานด้วย exit code 0 เพียงเพราะอ่านค่า config ที่ไม่ถูกต้อง unit จะหยุดทำงานและค้างอยู่ในสถานะหยุดทำงานนั้น โดยที่ systemctl status dsh จะแสดง inactive (dead) ซึ่งคุณสามารถตรวจสอบได้ ในขณะที่ Restart=always จะเปลี่ยนเหตุการณ์เดียวกันนั้นให้กลายเป็นวงจรการรีสตาร์ทที่ดูเหมือนปกติเมื่อมองจากระยะไกล
การจำกัดอัตรา (rate limit) คือส่วนที่ผู้คนมักมองข้าม ค่าเริ่มต้นของ systemd คือการเริ่มทำงาน 5 ครั้งภายใน 10 วินาที และด้วย RestartSec=5s คุณจะไม่มีทางเริ่มทำงานครบ 5 ครั้งภายในกรอบเวลา 10 วินาที ดังนั้น unit ที่ล่มตั้งแต่ตอนเริ่มทำงานจะรีสตาร์ทไปเรื่อยๆ โดยมีเพียง journal เท่านั้นที่ทราบเรื่องนี้ การใช้ StartLimitIntervalSec=300 ร่วมกับ StartLimitBurst=5 หมายความว่าหากเกิดความล้มเหลว 5 ครั้งภายใน 5 นาที ระบบจะถือว่าเพียงพอแล้ว: systemd จะยอมแพ้และพัก unit ไว้ในสถานะ 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 จะทำให้สถานะหายไปหลังจากการรีบูตครั้งถัดไป และการอัปเดต kernel จำเป็นต้องมีการรีบูตเสมอ
systemctl status dsh ควรแสดงสถานะ Active: active (running), Main PID และบรรทัด Memory: จากนั้นให้ยืนยันว่าบริการกำลังฟังพอร์ตอยู่ที่ใด:
sudo ss -lntp | grep 3080คุณควรเห็น 127.0.0.1:3080 หากคุณเห็น 0.0.0.0:3080 แสดงว่ามีบางอย่างแก้ไข bind address ทำให้ agent ของคุณเปิดรับการเชื่อมต่อจากสาธารณะ ชื่อกระบวนการในผลลัพธ์ดังกล่าวคือ node ไม่ใช่ dsh เนื่องจาก binary dsh เป็น Node script ทำให้ pgrep -x dsh ไม่พบกระบวนการดังกล่าว ให้ใช้ systemctl show -p MainPID dsh แทน
จากนั้นให้รีบูตเครื่องหนึ่งครั้ง บริการที่ไม่เคยผ่านการรีบูตยังไม่ถือว่าเป็นบริการที่สมบูรณ์
sudo rebootเชื่อมต่อกลับเข้ามาใหม่แล้วรัน systemctl is-active dsh ระบบจะแสดงผล active
การอ่าน log ด้วย journalctl
ทุกสิ่งที่ dsh เขียนไปยัง stdout และ stderr จะถูกบันทึกลงใน journal ภายใต้ชื่อ unit นั้นๆ
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 ใช้สำหรับกรองข้อมูลตามระดับความสำคัญ SyslogIdentifier=dsh ใน unit คือสาเหตุที่บรรทัดเหล่านั้นถูกแท็กเป็น dsh แทนที่จะเป็น node ซึ่งเป็นสิ่งสำคัญเมื่อคุณอ่านผลลัพธ์จาก journal ที่ไม่ได้กรองด้วย unit ในครั้งแรก
ตรวจสอบให้แน่ใจว่า journal จะยังคงอยู่หลังการ reboot ก่อนที่คุณจะจำเป็นต้องใช้งาน:
journalctl -u dsh -b -1หากคำสั่งดังกล่าวแสดงผลเป็น Specifying boot ID or boot offset has no effect, no persistent journal was found แสดงว่า journal ถูกเก็บไว้ใน /run และจะถูกลบหายไปทุกครั้งที่ reboot ให้สร้างไดเรกทอรีและเริ่มการทำงานของ daemon ใหม่:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journaldเข้าถึง UI ผ่าน SSH tunnel แทนการเปิดพอร์ตสาธารณะ
dsh ให้บริการ web UI บน 127.0.0.1:3080 และปฏิเสธที่จะให้บริการบนอินเทอร์เฟซอื่น หากคุณพยายามตั้งค่าให้ใช้ --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 command ได้ ดังนั้นหากพอร์ตนี้เข้าถึงได้จากภายนอก ก็เท่ากับเป็นการเปิด shell บน VPS ของคุณให้ใครก็ตามที่พบพอร์ตนี้ ผู้พัฒนาแจ้งว่าสาเหตุที่การ bind ถูกล็อกไว้ที่ loopback เป็นเพราะระบบยังไม่มีฟีเจอร์การยืนยันตัวตนจากระยะไกล ความหมายที่แท้จริงของบรรทัด 127.0.0.1:3080 ใน output ตอนเริ่มระบบ เป็นสิ่งที่ควรอ่านก่อนที่คุณจะพยายามย้ายพอร์ต ให้ใช้วิธี forward พอร์ตจากเครื่องของคุณแทน:
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 หมายถึงไม่ต้องรันคำสั่งใดๆ บนฝั่งรีโมท เพื่อให้ session คงสถานะ tunnel ไว้เท่านั้น ให้ปล่อยคำสั่งนี้ทำงานทิ้งไว้แล้วเปิด http://127.0.0.1:3080/ ในเบราว์เซอร์ของคุณ นี่คือจุดที่คุณต้องกรอก DeepSeek API key ในเมนู Settings แล้วเลือก Models และเป็นจุดที่คุณต้องเลือก workspace directory ให้ชี้ workspace ไปที่ /var/lib/dsh/workspace ซึ่งเป็นไดเรกทอรีที่ service user เป็นเจ้าของ มิฉะนั้นเครื่องมือจัดการไฟล์ของ 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ให้เลือกพอร์ต local อื่นด้วย 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 จึงทำหน้าที่เป็นปราการป้องกันเพียงหนึ่งเดียว: ควรใช้เฉพาะ key เท่านั้น ห้ามใช้การยืนยันตัวตนด้วยรหัสผ่าน และหลักการอื่นๆ ใน การเพิ่มความปลอดภัยให้ SSH บน VPS ควรถูกนำมาปรับใช้ให้เข้มงวดกว่าปกติ หาก VPS ของคุณขยายตัวจนกลายเป็นเครือข่ายส่วนตัวขนาดเล็กที่มีฐานข้อมูลหรือ staging box อยู่เบื้องหลัง การ ประกาศที่อยู่เหล่านั้นไปยัง tailnet ของคุณด้วย subnet router จะช่วยให้คุณไม่ต้องทำ forward พอร์ตแยกรายบริการ แม้ว่าการ bind แบบ loopback ของ dsh จะทำให้ UI ยังคงต้องเข้าผ่าน tunnel อยู่ดี
ไม่ควรเก็บ API key ไว้ใน unit file ค่าของ Environment= จะถูกแสดงออกมาด้วยคำสั่ง systemctl show dsh -p Environment ซึ่งผู้ใช้ทุกคนบนเครื่องสามารถรันได้ หากปลั๊กอินที่คุณติดตั้งจำเป็นต้องใช้ key ใน environment ให้เก็บไว้ใน /etc/dsh.env โดยตั้งค่า permission เป็น 600 และให้ root เป็นเจ้าของ จากนั้นอ้างอิงไฟล์ดังกล่าวด้วย EnvironmentFile=/etc/dsh.env ระบบ systemd จะอ่านไฟล์นั้นในฐานะ root ในขณะที่รันโปรแกรม และ systemctl show จะไม่แสดงเนื้อหาภายในไฟล์ออกมา ส่วนรายละเอียดว่าการตั้งค่าแต่ละอย่างจะถูกบันทึกลงในไฟล์ใดบนดิสก์ และข้อมูลใดบ้างที่จะถูกส่งออกจากเครื่องของคุณเมื่อคุณชี้ dsh ไปยัง Ollama endpoint ในเครื่องแทนที่จะเป็น DeepSeek API สามารถอ่านต่อได้ที่ การตั้งค่า key, models และ endpoints ของ dsh
ค่าใช้จ่ายในการรัน
การประมวลผล Inference เกิดขึ้นที่ API ของ DeepSeek ไม่ใช่บน VPS ของคุณ เซิร์ฟเวอร์ของคุณมีค่าใช้จ่ายสำหรับกระบวนการ Node, UI ที่ให้บริการ และทุกคำสั่งที่เอเจนต์ตัดสินใจรัน สองรายการแรกมีค่าคงที่และน้อยมาก ส่วนรายการที่สามไม่มีขีดจำกัดในไฟล์ unit นี้
ให้วัดค่าพื้นฐานบนเครื่องของคุณเองแทนการเชื่อตัวเลขจากผู้อื่น:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent มีหน่วยเป็นไบต์ ให้ตรวจสอบในขณะที่เอเจนต์กำลังทำงาน ไม่ใช่ขณะที่ไม่ได้ใช้งาน
การเรียกใช้เครื่องมือ (Tool calls) ถือเป็นกระบวนการลูกของบริการ จึงถูกนับรวมใน control group เดียวกันและถูกจำกัดภายใต้ขีดจำกัดเดียวกัน เอเจนต์ที่รัน npm install หรือชุดทดสอบภายใน workspace อาจใช้หน่วยความจำมากกว่าตัวโปรแกรมหลักอย่างมาก บน VPS ขนาด 1 GB นี่คือจุดที่ระบบมักจะล้มเหลว: เคอร์เนลจะเลือกกระบวนการหนึ่งขึ้นมาและสั่งยุติการทำงาน และ journalctl -k | grep -i "out of memory" จะแสดงบรรทัด Out of memory: Killed process ที่ระบุว่าเคอร์เนลเลือกยุติกระบวนการใด ซึ่งบ่อยครั้งกระบวนการนั้นไม่ใช่ต้นเหตุของปัญหา
วิธีแก้ไขคือการกำหนดขีดจำกัดด้วยตนเอง การใช้ MemoryMax= และ CPUQuota= ในส่วน [Service] จะช่วยจำกัดความเสียหายให้อยู่ภายใน unit นั้นๆ เพื่อให้การ build ที่ทำงานผิดปกติถูกยุติการทำงานแทนที่จะทำให้ทั้งเซิร์ฟเวอร์ค้าง การจำกัดหน่วยความจำและ CPU ด้วย systemd ครอบคลุมถึงตัวเลขและพฤติกรรมเมื่อเกิดความล้มเหลว พื้นที่ดิสก์ก็จะเพิ่มขึ้นเช่นกันจากประวัติเซสชันภายใต้ DSH_HOME และจากสิ่งที่เอเจนต์เขียนลงใน workspace ดังนั้นให้เพิ่ม du -sh /var/lib/dsh ลงในเครื่องมือที่คุณใช้ตรวจสอบพื้นที่ดิสก์อยู่แล้ว
หากสิ่งที่คุณต้องการคือเอเจนต์แบบโต้ตอบที่คุณสามารถเชื่อมต่อและตัดการเชื่อมต่อได้ การรันเป็น service อาจไม่เหมาะสม และ การรันเอเจนต์ใน tmux session แบบถาวร จะตอบโจทย์มากกว่า ให้รัน dsh เป็น unit ในกรณีที่คุณต้องการให้มันทำงานตลอดเวลาและเข้าถึงได้ผ่าน tunnel
รูปแบบความล้มเหลวและข้อความที่คุณจะพบ
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. ยูนิตถึงขีดจำกัดอัตราการเริ่มทำงาน (start rate limit) จึงหยุดทำงาน ข้อผิดพลาดที่แท้จริงจะปรากฏอยู่ในบรรทัดก่อนหน้านั้น ให้รัน sudo systemctl reset-failed dsh ก่อนที่จะลองใหม่อีกครั้ง
ยูนิตมีสถานะเป็น active (running) แต่เบราว์เซอร์ไม่แสดงผลใดๆ. ให้ตรวจสอบบน VPS หาก curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up แสดงผลเป็น up แสดงว่าบริการทำงานปกติ และปัญหาอยู่ที่การทำ port forward
การอัปเกรดอย่างตั้งใจ
การทำ Pinning หมายความว่าการอัปเกรดเป็นสิ่งที่คุณเลือกทำเอง ไม่ใช่สิ่งที่เกิดขึ้นโดยที่คุณไม่ได้ควบคุม ให้อ่าน release notes ก่อนเสมอ เพราะคำเตือนของผู้พัฒนาต้นทางเกี่ยวกับความเปลี่ยนแปลงที่ทำให้ระบบไม่รองรับกับเวอร์ชันเดิมคือเหตุผลหลักที่ต้องทำ Pinning ให้สำรองข้อมูลใน state directory จากนั้นจึงเปลี่ยนเวอร์ชัน:
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การย้อนกลับ (Rollback) คือกระบวนการเดียวกันกับ npm install -g โดยใช้เวอร์ชันเก่า พร้อมกับการกู้คืนไฟล์ tarball ซึ่งจะทำได้ก็ต่อเมื่อคุณได้สำรองข้อมูลไว้แล้วเท่านั้น สำหรับ agent runtime ในขั้นตอน preview นั้น เป็นซอฟต์แวร์ประเภทที่การอัปเกรดอาจเขียนทับรูปแบบไฟล์ config ของคุณโดยที่คุณไม่ทันตั้งตัว
FAQ
ทำไม dsh ถึงหยุดทำงานเมื่อฉันปิดเซสชัน SSH?
เพราะ npx @deepseek-ai/dsh web เป็นกระบวนการเบื้องหน้า (foreground process) ที่ขึ้นตรงกับเซสชันการล็อกอินของคุณ จึงถูกยุติการทำงานเมื่อเซสชันสิ้นสุดลง ในขณะที่ systemd unit จะขึ้นตรงกับระบบ init ทำให้ยังคงทำงานต่อไปหลังจากคุณตัดการเชื่อมต่อและเริ่มทำงานใหม่หลังจากรีบูตเครื่อง sudo systemctl enable --now dsh คือขั้นตอนคู่ที่ช่วยให้คุณได้ทั้งสองอย่าง คือ enable สำหรับการรีบูต และ --now สำหรับการเริ่มทำงานในครั้งนี้
ฉันควรใช้ Type=simple หรือ Type=exec สำหรับ dsh?
Type=exec dsh ทำงานในเบื้องหน้าและไม่มีการ fork กระบวนการ ดังนั้นใช้ได้ทั้งสองแบบ แต่ Type=exec จะทำให้ systemd รอจนกว่า execve() จะทำงานสำเร็จก่อนจึงจะถือว่าการเริ่มบริการเสร็จสมบูรณ์ หากระบุ path ใน ExecStart= ผิดพลาด systemctl start จะแจ้งเตือนด้วย status=203/EXEC ให้คุณเห็นทันที แต่ถ้าใช้ Type=simple ความผิดพลาดเดียวกันจะถูกรายงานว่าสำเร็จและซ่อนอยู่ใน journal ส่วน Type=forking และ Type=notify นั้นไม่ถูกต้องทั้งคู่ และจะค้างจนกว่า TimeoutStartSec จะหมดเวลาหลังจาก 90 วินาที
ฉันจะเปิด dsh web UI จากแล็ปท็อปได้อย่างไร?
ให้ทำ port forwarding ผ่าน SSH ด้วยคำสั่ง ssh -N -L 3080:127.0.0.1:3080 you@your-vps จากนั้นเปิด http://127.0.0.1:3080/ ในเบราว์เซอร์ของคุณ อย่าพยายามผูกบริการเข้ากับ public address โดยตรง dsh จะปฏิเสธ --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 รันคำสั่ง 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 เพื่อแก้ไข
ฉันควรล็อกเวอร์ชันของ dsh ไว้ใน unit หรือไม่?
ควรใช้เวอร์ชันที่ npm view @deepseek-ai/dsh version รายงานเมื่อคุณตั้งค่าบริการ โดยติดตั้งด้วย npm install -g @deepseek-ai/dsh@<that version> และบันทึกไว้ในที่ที่คุณสามารถค้นหาได้ 0.1.0-rc.7 เป็นเวอร์ชันล่าสุด ณ วันที่ 18 สิงหาคม 2026 ประเด็นสำคัญไม่ใช่ตัวเลขเวอร์ชัน แต่คือการที่ npx โดยไม่ระบุเวอร์ชันจะทำการดึงแพ็กเกจใหม่ ณ เวลาที่เริ่มทำงาน ซึ่งอาจทำให้การรีสตาร์ทแบบอัตโนมัติเปลี่ยนไปใช้ build ที่มีรูปแบบ config ต่างออกไปโดยที่คุณไม่รู้ตัว