วิธีรัน 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แพ็กเกจของ Ubuntu 24.04 เองคือ Node 18 (18.19.1 ณ เดือนสิงหาคม 2026) ซึ่งถือว่าเก่าสำหรับแพ็กเกจที่เผยแพร่ในปีนี้ ตัว npm เองมีการตรวจสอบเวอร์ชันของ Node โดยเอกสารระบุว่าฟิลด์ engines ของแพ็กเกจ "จะแสดงเพียงคำเตือนเท่านั้น" เว้นแต่คุณจะเปิดใช้งาน engine-strict ปัญหาคือไฟล์ manifest @deepseek-ai/dsh ที่เผยแพร่นั้นไม่มีฟิลด์ engines อยู่เลย (ตรวจสอบเมื่อวันที่ 27 ตุลาคม 2026 สำหรับ 0.1.0-rc.7, 0.2.1-rc.2 และ 0.2.1-rc.4) แม้ว่าใน repository จะมีการประกาศไว้ก็ตาม ดังนั้น npm จึงไม่มีข้อมูลสำหรับเปรียบเทียบกับ Node ของคุณและไม่แสดงคำเตือน EBADENGINE ใดๆ ความล้มเหลวจะเกิดขึ้นในขณะรันไทม์แทนในรูปแบบของ syntax error หรือฟังก์ชัน built-in ที่หายไป ซึ่งเป็นจุดที่ตรวจพบปัญหาได้ยากกว่ามาก
ข้อกำหนดขั้นต่ำของ repository ก็มีการเปลี่ยนแปลงเช่นกัน ซอร์สโค้ด 0.2.1-rc.2 รองรับ ^22.19.0 || >=24.0.0 แต่แพ็กเกจ CLI บน branch master ได้ประกาศ >=24.9.0 ซึ่ง Node 22 ไม่ผ่านเกณฑ์ดังกล่าว ให้ติดตั้ง Node 24 จาก NodeSource ซึ่งเป็นรุ่น long term support (LTS) ที่อยู่ในสถานะบำรุงรักษาตั้งแต่วันที่ 20 ตุลาคม 2026 และบิลด์ปัจจุบันก็รองรับทั้งสองช่วงเวอร์ชัน ส่วน Node 26 จะเข้าสู่สถานะ LTS ในวันที่ 28 ตุลาคม 2026
curl -fsSL https://deb.nodesource.com/setup_24.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 ควรแสดงเวอร์ชัน v24 เป็น 24.9.0 หรือใหม่กว่า บรรทัด less มีไว้เพื่อให้คุณตรวจสอบสคริปต์ก่อน เพราะการส่งสคริปต์จากระยะไกลเข้าสู่ bash โดยตรงจะเป็นการรันโค้ดที่คุณไม่ได้อ่านตรวจสอบก่อนใช้งาน
ตรวจสอบว่าโปรแกรมทำงานได้ก่อนเขียน unit
npx @deepseek-ai/dsh@0.2.1-rc.2 webปล่อยให้โปรแกรมทำงานต่อไป จากนั้นเปิด SSH session ที่สอง:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/รหัสสถานะ HTTP ใดๆ หมายความว่า web profile กำลังฟังอยู่ที่ loopback ซึ่งเป็นค่าเริ่มต้นที่โปรแกรมผูกไว้ ให้คาดหวังผลลัพธ์เป็น 401 และถือว่าผ่าน: UI จะตรวจสอบสิทธิ์เบราว์เซอร์ด้วย launch token ใน URL ที่แสดงออกมา ซึ่งจะถูกนำไปแลกเป็น signed session cookie ดังนั้นคำขอที่ไม่มีทั้งสองอย่างจะได้รับรหัส 401 ส่วน 000 ร่วมกับ curl: (7) Failed to connect to 127.0.0.1 port 3080 หมายความว่าโปรแกรมไม่ได้เปิดฟังอยู่ และเทอร์มินัลแรกจะแจ้งสาเหตุให้คุณทราบ ให้หยุดการรันด้วยตนเองด้วย Ctrl+C ก่อนดำเนินการต่อ เพราะ unit ที่พยายามผูกพอร์ตที่ถูกใช้งานอยู่แล้วจะล้มเหลวด้วย Error: listen EADDRINUSE: address already in use 127.0.0.1:3080
0.1.0-rc.7 คือเวอร์ชันที่เผยแพร่ ณ วันที่ 18 สิงหาคม 2026 ซึ่งเก่ากว่าระบบตรวจสอบสิทธิ์ผ่านเบราว์เซอร์ที่คู่มือนี้อ้างอิง: ไฟล์ README ของเว็บระบุว่าการป้องกันคือการตรวจสอบ Host และ "ตัวนำส่งเว็บไม่มีเลเยอร์ตรวจสอบสิทธิ์" เมื่อวันที่ 11 ตุลาคม 2026 แท็ก latest ชี้ไปที่ 0.2.0-rc.2 ส่วนวันที่ 27 ตุลาคม 2026 latest ชี้ไปที่ 0.2.1-rc.2 และ next ชี้ไปที่ 0.2.1-rc.4 ดังนั้นคำสั่งในที่นี้จึงล็อกเวอร์ชันไว้ที่ 0.2.1-rc.2 ให้ตรวจสอบเวอร์ชันปัจจุบันด้วย npm view @deepseek-ai/dsh version แล้วจึงล็อกเวอร์ชันที่คุณตัดสินใจจะใช้งาน
ติดตั้งเวอร์ชันที่คุณกำหนดไว้แบบ global
npx ไม่ใช่เครื่องมือที่เหมาะสมสำหรับใช้ภายใน unit file เนื่องจากมันจะทำการระบุเวอร์ชันของแพ็กเกจใหม่ทุกครั้งที่ process เริ่มทำงาน ส่งผลให้การรีสตาร์ทในอีก 3 เดือนข้างหน้าอาจทำให้ agent เวอร์ชัน preview ถูกเรียกขึ้นมาทำงานโดยที่คุณไม่ได้เปลี่ยนแปลงอะไรเลย นอกจากนี้ยังจำเป็นต้องเข้าถึง npm registry ในขณะบูตเครื่อง ซึ่งจะทำให้เครื่องที่เคยทำงานปกติกลายเป็น unit ที่ล้มเหลวในวันที่ registry ทำงานช้า ให้ทำการติดตั้งเพียงครั้งเดียวด้วยเวอร์ชันที่คุณระบุไว้เท่านั้น:
sudo npm install -g @deepseek-ai/dsh@0.2.1-rc.2
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
ผู้ใช้ที่เป็นเจ้าของ service และไม่มีสิทธิ์อื่น
agent จะรัน shell commands ซึ่งเป็นหน้าที่ของมัน การรัน agent ในฐานะ root ทำให้ทุก tool call มีสิทธิ์ระดับ root ดังนั้นให้สร้าง account เฉพาะสำหรับ agent และกำหนดให้ไม่มี 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 จะกลายเป็น DSH_HOME ซึ่งเป็น directory ที่ dsh ใช้เก็บ profiles profile คือชุด plugin bundles ที่มีชื่อเรียกและมี patch layer ของคุณอยู่ด้านบน ส่วน profiles web และ headless จะสร้างตัวเองจาก templates ที่มากับระบบเมื่อ boot ครั้งแรก สิ่งใดก็ตามที่คุณเพิ่มเข้าไปใน stack ภายหลังจะทำงานในฐานะ user นี้ และใช้ file access กับ shell access ของ agent เอง ดังนั้น การตรวจสอบ plugin ก่อนติดตั้ง จึงเป็นงานประเภทเดียวกับการสร้าง account การ boot ครั้งแรกจะเขียนไฟล์และอาจ fetch bundles ดังนั้นให้ทำด้วยตนเองในจุดที่คุณสามารถ monitor ได้
sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile webกำหนด HOME อย่างชัดเจน แทนการเชื่อว่า sudo จะจัดการค่าดังกล่าวอย่างไร เพราะการที่ sudo จะ rewrite HOME สำหรับคำสั่งที่ไม่ใช่ login command หรือไม่นั้น ขึ้นอยู่กับการตั้งค่า set_home ใน /etc/sudoers หากกำหนดผิด การรันครั้งแรกจะสร้าง cache directories ใน home directory ของ คุณ โดยมี dsh เป็นเจ้าของ และ service จะหา state ของตัวเองไม่พบในภายหลัง หยุดด้วย Ctrl+C เมื่อการตรวจสอบ curl แสดง status codeแล้ว
ไฟล์ 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 ทำงาน 2 อย่าง enable ใช้ทำให้บริการกลับมาเริ่มทำงานหลัง reboot และ --now ใช้เริ่มบริการในการบูตครั้งนี้ การใช้ systemctl start โดยไม่มีการตั้งค่าเพิ่มเติมจะมีผลจนถึงการ reboot ครั้งถัดไปเท่านั้น และการอัปเดต kernel ทำให้ต้อง reboot
systemctl status dsh ควรแสดงบรรทัด Active: active (running), Main PID และ Memory: จากนั้นยืนยันว่าบริการกำลังฟังอยู่ที่ใด:
sudo ss -lntp | grep 3080ค่าที่ต้องการคือ 127.0.0.1:3080 หากเห็น address อื่น แสดงว่า flag --host ทำให้ listener ย้ายไปยัง network ที่ address นั้นอยู่ และให้บริการ HTTP แบบไม่เข้ารหัส เว้นแต่จะตั้งค่า TLS ไว้ ชื่อ process ในผลลัพธ์นั้นคือ node ไม่ใช่ dsh เนื่องจาก binary dsh เป็นสคริปต์ Node ดังนั้น pgrep -x dsh จึงไม่พบ process ให้ใช้ systemctl show -p MainPID dsh แทน
จากนั้น reboot 1 ครั้ง บริการที่ยังไม่เคยทำงานต่อได้หลัง reboot ยังไม่ถือว่าเป็นบริการที่พร้อมใช้งาน
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 (user interface) บน 127.0.0.1:3080 ค่าเริ่มต้นคือ loopback ซึ่งไม่ใช่ทางเลือกเดียว: --host ยอมรับที่อยู่ IPv4 หรือ IPv6 ที่ระบุชัดเจนของอินเทอร์เฟซในเครื่อง หากใช้ที่อยู่อื่นที่ไม่ใช่ loopback ระบบจะให้บริการผ่านเครือข่ายด้วย HTTP ธรรมดาพร้อมคำเตือนเมื่อเริ่มทำงาน เว้นแต่คุณจะระบุ --tls-cert และ --tls-key เพิ่มเติม สิ่งที่ dsh ปฏิเสธคือการใช้ wildcard หากคุณระบุ --host 0.0.0.0 ข้อผิดพลาดจะแสดงดังนี้:
webserver: host "0.0.0.0" is an unspecified (wildcard) address, which is not supported:ข้อความระบุว่า "การผูกกับทุกอินเทอร์เฟซจะทำให้เกิดความเสี่ยงต่อการรันโค้ดจากระยะไกลบนเครือข่าย" และแนะนำให้ "ผูกกับที่อยู่ IPv4 หรือ IPv6 ที่ระบุชัดเจนของอินเทอร์เฟซในเครื่องแทน" หากค่าที่ระบุไม่ใช่ IP literal เช่น ชื่อโฮสต์ ระบบจะล้มเหลวด้วย is not a concrete IPv4 or IPv6 address literal
แนะนำให้คงค่าเริ่มต้นไว้ Web API (application programming interface) ทำหน้าที่ควบคุม agent และ agent จะรันคำสั่ง shell dsh มีการตรวจสอบสิทธิ์หน้า API ดังกล่าวด้วยการตรวจสอบ Host และ Origin รวมถึง launch token ใน URL ที่แสดงตอนเริ่มต้นซึ่ง UI จะนำไปแลกเป็น session cookie ที่ลงนามแล้ว อย่างไรก็ตาม หากใช้ HTTP ธรรมดา token ดังกล่าวจะถูกส่งผ่านเครือข่ายในรูปแบบข้อความธรรมดา และเอกสารของผู้พัฒนาเองระบุว่าการตรวจสอบ Host ไม่ได้ป้องกันพอร์ตที่เปิดรับอยู่ หากคุณต้องการใช้ UI บนชื่อโฮสต์จริง คู่มือการใช้งานสาธารณะของ DeepSeek แนะนำให้ใช้ reverse proxy ร่วมกับ --public-url และ --trusted-host หรือใช้ TLS โดยตรงด้วย --tls-cert และ --tls-key การอ่าน ความหมายของบรรทัด 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 ไว้เท่านั้น ปล่อยให้มันทำงานต่อไป ค้นหา URL ที่ dsh แสดงตอนเริ่มต้นด้วย journalctl -u dsh -n 100 --no-pager แล้วเปิดในเบราว์เซอร์ของคุณ URL นั้นจะมี launch token อยู่ หากคุณเปิด http://127.0.0.1:3080/ โดยตรงในเบราว์เซอร์ที่ยังไม่ได้ลงชื่อเข้าใช้ คุณจะได้รับข้อผิดพลาด 401 token ดังกล่าวผูกกับ process ที่กำลังทำงานอยู่ แต่ session cookie ที่ได้มาจะมีอายุยาวนานกว่า: cookie จะถูกลงนามด้วย secret ที่ dsh เก็บไว้ใน $DSH_HOME/.credentials.yaml และมีอายุ 30 วันโดยค่าเริ่มต้น ดังนั้นเบราว์เซอร์ที่เคยลงชื่อเข้าใช้แล้วจึงสามารถเปิด URL โดยตรงได้อีกครั้งแม้จะมีการรีสตาร์ท คุณจำเป็นต้องใช้ URL ใหม่จาก journal เฉพาะเมื่อใช้เบราว์เซอร์ใหม่หรือเมื่อ cookie หมดอายุเท่านั้น ให้ถือว่า URL เป็นข้อมูลรับรอง (credential): ผู้พัฒนาแนะนำให้แชร์เฉพาะกับผู้ใช้ที่ตั้งใจไว้เท่านั้น และใครก็ตามที่อ่าน system journal ได้ก็จะเห็น URL นี้ เมื่อเข้าสู่ระบบได้แล้ว ให้ใส่ DeepSeek API key ที่เมนู Settings แล้วเลือก Models จากนั้นเลือกไดเรกทอรี workspace ให้ชี้ 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 จากนั้นเปิด URL ที่แสดงโดยเปลี่ยน 3080 เป็น 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 พอร์ตแยกรายบริการ แม้ว่า dsh จะใช้ค่าเริ่มต้นเป็น loopback แต่ UI ก็ยังคงเข้าถึงผ่าน tunnel ได้
ไม่ควรเก็บ key ไว้ใน unit file ค่าของ Environment= จะถูกแสดงโดย systemctl show dsh -p Environment ซึ่งผู้ใช้ทุกคนบนเครื่องสามารถรันได้ หาก plugin ที่คุณติดตั้งต้องการ key ใน environment ให้ใส่ไว้ใน /etc/dsh.env โดยตั้งค่าสิทธิ์เป็น 600 และให้ root เป็นเจ้าของ จากนั้นอ้างอิงไฟล์ด้วย EnvironmentFile=/etc/dsh.env systemd จะอ่านไฟล์นั้นในฐานะ root ขณะรัน และ systemctl show จะไม่แสดงเนื้อหาภายในไฟล์ออกมา การตั้งค่าแต่ละอย่างจะถูกบันทึกไว้ที่ไฟล์ใดบนดิสก์ และข้อมูลใดบ้างที่จะถูกส่งออกจากเครื่องของคุณเมื่อคุณชี้ dsh ไปที่ Ollama endpoint ในเครื่องแทนที่จะเป็น API ของ DeepSeek เป็นหัวข้อที่อธิบายไว้ใน การตั้งค่า 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 ไม่สามารถรันไฟล์ได้ และ log แสดง Failed to locate executable /usr/local/bin/dsh: No such file or directory เส้นทางใน ExecStart= ไม่ตรงกับสิ่งที่ command -v dsh แสดงไว้ นี่คือความล้มเหลวที่ Type=exec รายงานในเวลา systemctl start แทนที่จะซ่อนไว้
status=217/USER. systemd ล้มเหลวในการระบุหรือเปลี่ยนข้อมูลประจำตัวผู้ใช้จาก User= และ Group= หรือล้มเหลวในการตั้งค่า user namespacing สำหรับ unit นี้ สาเหตุทั่วไปคือบัญชีผู้ใช้ไม่มีอยู่จริง ให้ยืนยันด้วย id dsh
status=200/CHDIR. WorkingDirectory= หายไป หรือผู้ใช้ของ service ไม่สามารถเข้าถึงได้ sudo -u dsh ls /var/lib/dsh/workspace จะจำลองเหตุการณ์นี้โดยตรง
Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. มีบางอย่างใช้งานพอร์ตนั้นอยู่แล้ว โดยปกติเกิดจาก npx ที่ค้างอยู่ใน terminal อื่น 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. unit ถึงขีดจำกัดอัตราการเริ่มทำงาน (start rate limit) และยอมแพ้ ข้อผิดพลาดที่แท้จริงจะอยู่ในบรรทัดก่อนหน้า ให้รัน sudo systemctl reset-failed dsh ก่อนที่จะลองใหม่อีกครั้ง
Unit มีสถานะเป็น active (running) แต่เบราว์เซอร์ไม่แสดงผลใดๆ. ให้รันการตรวจสอบบน VPS: หาก curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/ แสดงรหัสสถานะที่นั่น แสดงว่า listener ตอบสนองบน VPS แล้ว ให้ตรวจสอบการส่งต่อพอร์ต (port forward) ถัดไป รหัสนั้นไม่ได้พิสูจน์ว่าทุกอย่างโหลดเสร็จสมบูรณ์ เนื่องจาก upstream ระบุว่าความล้มเหลวของปลั๊กอินเสริมไม่ทำให้สถานะความพร้อมล้มเหลว ดังนั้นให้อ่าน journalctl -u dsh -n 200 --no-pager เพื่อดูข้อผิดพลาดของปลั๊กอินด้วย รหัส 401 ในเบราว์เซอร์หมายความว่าไม่มี session cookie ที่ถูกต้องและ URL ไม่มี launch token ที่ถูกต้อง ซึ่งอาจเกิดจากการเปิด URL เปล่าๆ หรือ token มาจากก่อนการรีสตาร์ทครั้งล่าสุด ให้เปิด URL ที่กระบวนการปัจจุบันแสดงออกมา
การอัปเกรดโดยเจตนา
การทำ Pinning หมายความว่าการอัปเกรดเป็นสิ่งที่คุณเลือกทำเอง ไม่ใช่สิ่งที่เกิดขึ้นโดยที่คุณไม่ได้ตั้งตัว โปรดอ่านบันทึกประจำรุ่น (release notes) ก่อนเสมอ เพราะคำเตือนของผู้พัฒนาต้นทางเกี่ยวกับความเปลี่ยนแปลงที่ส่งผลต่อความเข้ากันได้ (compatibility-breaking changes) คือเหตุผลหลักที่ต้องทำ 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.2.1-rc.2
sudo systemctl start dsh
journalctl -u dsh -n 50 --no-pagerการย้อนกลับ (rolling back) คือกระบวนการเดียวกันกับ npm install -g โดยใช้เวอร์ชันเก่าควบคู่ไปกับการกู้คืนไฟล์ tarball ซึ่งจะทำได้ก็ต่อเมื่อคุณได้สำรองข้อมูลไว้เท่านั้น รันไทม์ของเอเจนต์ในขั้นตอนพรีวิว (preview-stage agent runtime) เป็นซอฟต์แวร์ประเภทที่การอัปเกรดอาจเขียนทับรูปแบบไฟล์คอนฟิกของคุณโดยที่คุณไม่ทันตั้งตัว
FAQ
ทำไม dsh ถึงหยุดทำงานเมื่อฉันปิดเซสชัน SSH?
เพราะ npx @deepseek-ai/dsh web เป็นกระบวนการที่ทำงานใน foreground ซึ่งถูกควบคุมโดยเซสชันการล็อกอินของคุณ ดังนั้นมันจึงถูกปิดลงเมื่อเซสชันสิ้นสุดลง ในขณะที่ systemd unit จะถูกควบคุมโดย init system แทน ซึ่งเป็นเหตุผลว่าทำไมมันจึงยังคงทำงานต่อไปหลังจากที่คุณตัดการเชื่อมต่อและเริ่มทำงานใหม่หลังจากรีบูต sudo systemctl enable --now dsh คือขั้นตอนคู่ที่ช่วยให้คุณได้ทั้งสองอย่าง: enable สำหรับการรีบูต และ --now สำหรับการบูตครั้งนี้
ฉันควรใช้ Type=simple หรือ Type=exec สำหรับ dsh?
Type=exec dsh ทำงานใน foreground และไม่มีการ fork ดังนั้นทั้งสองแบบจึงใช้งานได้ แต่ Type=exec จะทำให้ systemd รอให้ execve() ทำงานสำเร็จก่อนที่จะถือว่าการเริ่มต้นนั้นสำเร็จ หากมี path ที่ผิดใน ExecStart= จะทำให้ systemctl start ล้มเหลวพร้อมแสดง status=203/EXEC ให้คุณเห็น แต่ถ้าใช้ Type=simple ความผิดพลาดเดียวกันจะส่งค่ากลับว่าสำเร็จและซ่อนอยู่ใน journal ส่วน Type=forking และ Type=notify นั้นไม่ถูกต้องทั้งคู่ในกรณีนี้ และทั้งสองแบบจะค้างจนกว่า TimeoutStartSec จะหมดเวลาหลังจาก 90 วินาที
ฉันจะเปิด web UI ของ dsh จากแล็ปท็อปได้อย่างไร?
ให้ทำ port forwarding ผ่าน SSH ด้วยคำสั่ง ssh -N -L 3080:127.0.0.1:3080 you@your-vps จากนั้นเปิด URL ที่ dsh แสดงตอนเริ่มต้น ซึ่งคุณสามารถหาได้ด้วย journalctl -u dsh ในเบราว์เซอร์ของคุณ URL นั้นจะมี launch token ติดมาด้วย และเบราว์เซอร์ที่ยังไม่ได้ล็อกอินจะได้รับข้อผิดพลาด 401 หากไม่มี token ดังกล่าว เมื่อล็อกอินแล้ว session cookie จะมีอายุ 30 วันโดยค่าเริ่มต้นและยังคงอยู่แม้จะมีการรีสตาร์ท service ให้คงค่า service ไว้ที่ loopback ตามค่าเริ่มต้น dsh จะปฏิเสธ wildcard เช่น --host 0.0.0.0 เนื่องจากเป็นที่อยู่ที่ไม่ได้ระบุไว้ และแม้ว่ามันจะยอมรับที่อยู่ภายในเครื่องที่ระบุชัดเจนหนึ่งแห่ง แต่การ bind ที่ไม่ใช่ loopback จะเป็นการให้บริการผ่าน HTTP ธรรมดา เว้นแต่คุณจะตั้งค่า TLS ซึ่งหากไม่ทำเช่นนั้นจะเป็นการส่ง launch token สำหรับเอเจนต์ที่รันคำสั่ง shell ผ่านเครือข่ายในรูปแบบข้อความธรรมดา (cleartext)
ฉันสามารถรัน dsh ในฐานะ root เพื่อให้จัดการสิทธิ์ได้ง่ายขึ้นได้หรือไม่?
ไม่ได้ ตัว harness ถูกสร้างขึ้นเพื่อรันคำสั่งและเขียนไฟล์ ดังนั้นสิทธิ์ใดก็ตามที่ service มี เอเจนต์ก็จะมีสิทธิ์นั้นด้วย ให้สร้างบัญชีผู้ใช้ระบบด้วย 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 รายงานเมื่อคุณตั้งค่า service โดยติดตั้งด้วย npm install -g @deepseek-ai/dsh@<that version> และบันทึกไว้ในที่ที่คุณสามารถหาได้ง่าย 0.1.0-rc.7 เป็นเวอร์ชันปัจจุบัน ณ วันที่ 18 สิงหาคม 2026, 0.2.0-rc.2 ณ วันที่ 11 ตุลาคม 2026 และ 0.2.1-rc.2 ณ วันที่ 27 ตุลาคม 2026 โดยที่ next ได้ไปถึง 0.2.1-rc.4 แล้ว รุ่นที่เก่าถึง 0.1.0-rc.7 จะไม่มีระบบยืนยันตัวตนผ่านเบราว์เซอร์ ดังนั้นอย่าล็อกเวอร์ชันที่เก่าถึงหรือเก่ากว่านั้นสำหรับโปรไฟล์เว็บ ประเด็นสำคัญไม่ใช่ตัวเลข แต่คือการที่ npx ที่ไม่มีการระบุเวอร์ชันจะทำการดึงแพ็กเกจใหม่ ณ เวลาที่เริ่มทำงาน ดังนั้นการรีสตาร์ทแบบอัตโนมัติอาจทำให้คุณเปลี่ยนไปใช้ build ที่มีรูปแบบ config ต่างออกไปโดยไม่รู้ตัว