Cách mở DSH web UI trên VPS từ laptop
DSH bind vào 127.0.0.1:3080 nên URL in trên VPS không mở từ laptop. Xem 3 cách truy cập an toàn và cách không nên dùng để tránh lộ launch token.
Vì sao DSH web UI không mở được từ laptop của bạn
DSH web UI bind vào loopback interface, nên URL được terminal in ra chỉ hoạt động trên chính máy đã in URL đó. npx @deepseek-ai/dsh web báo một launch URL có dạng http://127.0.0.1:3080/?token=..., trong đó 127.0.0.1 có nghĩa là “máy tính đang đọc địa chỉ này”, bất kể máy tính nào đọc nó. Laptop của bạn đọc URL, kiểm tra loopback interface của chính nó và không tìm thấy tiến trình nào đang listening ở đó. Nếu phần gây nhầm lẫn là chính địa chỉ này, bài viết vì sao dsh in http://127.0.0.1:3080 ngay từ đầu giải thích chi tiết hơn. Không thay đổi địa chỉ mà DSH bind vào. Thay vào đó, hãy tạo một đường truy cập có xác thực từ laptop đến loopback address của VPS.
Thiết lập loopback mặc định là đúng và mọi phương thức trên trang này đều giữ nguyên thiết lập đó. Phía sau port 3080 là một agent chạy shell command trên server của bạn, còn web UI không có login page. Hiện tại DSH có xác thực browser bằng launch token: mỗi lần process khởi động sẽ tạo một token mới, URL được in ra sẽ chứa token đó, và lần truy cập đầu tiên sẽ đổi token thành session cookie. Token này là một bearer credential duy nhất nằm trong URL, nên loopback vẫn là lớp quan trọng nhất: để truy cập được socket, trước hết bạn phải có shell trên máy đó.
DSH bind vào đâu và cách kiểm tra cấu hình của bạn
Được kiểm tra vào ngày 11 October 2026, README của DeepSeek Harness ghi rằng npx @deepseek-ai/dsh web “khởi động Web UI tại http://127.0.0.1:3080 theo mặc định”. Tài liệu CLI trong cùng repository cho biết port mặc định vẫn là 3080, rằng --host “phải chỉ rõ một địa chỉ cục bộ cụ thể, không được là wildcard”, và --host 0.0.0.0 sẽ thoát với lỗi sử dụng. DSH đang ở giai đoạn developer preview, và README cảnh báo bằng chữ in hoa rằng sẽ có các thay đổi gây mất compatibility. Hãy xác nhận cả hai giá trị trên chính installation của bạn thay vì tin vào bất kỳ trang nào, kể cả trang này.
dsh --profile web --dump-config
ss -ltnp | grep 3080--dump-config in ra cây cấu hình đã ghép mà không khởi động agent, nên nó hiển thị host và port còn hiệu lực sau mọi lớp patch. ss cho biết hiện tại process nào thực sự đang listen. Một dòng hoạt động bình thường có dạng:
LISTEN 0 511 127.0.0.1:3080 0.0.0.0:* users:(("node",pid=8123,fd=24))Đọc địa chỉ trước dấu hai chấm và bỏ qua mọi thứ khác. 127.0.0.1:3080 chỉ là loopback, đúng với yêu cầu. Bất kỳ địa chỉ nào khác có nghĩa là DSH đang phục vụ network mà địa chỉ đó thuộc về, và network đó có thể là public network. Nếu ss không in ra dòng nào, DSH chưa chạy; tunnel cũng không giúp được cho đến khi DSH chạy. Cách đạt đến trạng thái đó được trình bày trong cài đặt và chạy DeepSeek Harness trên VPS.
Các flag chỉ áp dụng cho một lần chạy. Nếu muốn giá trị được giữ lại, hãy sửa lớp profile patch. dsh --profile <name> khởi động profile tại $DSH_HOME/profiles/<name>, và các lớp được áp dụng theo thứ tự sau: bundle patch, cordis.patch.yml riêng của profile, $DSH_HOME/cordis.patch.yml ở cấp home, rồi mọi overlay --patch. Các thiết lập listen nằm trong plugin @deepseek-ai/dsh-host-webserver, tại host và port. Hãy đọc cây cấu hình đã ghép của bạn bằng --dump-config trước khi chỉnh sửa, vì web profile tự tạo lại từ template được đóng gói ở lần khởi động đầu tiên, còn bản preview vẫn liên tục thay đổi cấu trúc của template đó.
Dòng khởi động cũng là nơi lấy credential của bạn. DSH in root URL kèm process token trong query token, và chỉ GET /?token=... mới chấp nhận token đó: không phải API path và cũng không phải header Authorization. Request đó đặt một cookie đã ký, chỉ dành cho host, loại HttpOnly, SameSite=Strict rồi redirect đến URL sạch. Token thay đổi sau mỗi lần restart và không được lưu trữ; thời hạn của cookie mặc định tối đa là 30 ngày. Request không có credential hợp lệ sẽ nhận đúng một phản hồi 401 tối giản. Hãy copy toàn bộ URL được in ra, bao gồm cả token, vì mọi phương pháp bên dưới đều kết thúc bằng việc mở URL đó.
Cổng kiểm tra thứ hai: hàng rào tin cậy của /api
Đưa được packet đến cổng 3080 mới chỉ giải quyết một nửa vấn đề. DSH còn một lần kiểm tra thứ hai. Lỗi này thường gây nhầm lẫn: trang tải được, giao diện hiển thị, rồi không có chức năng nào hoạt động.
Plugin @deepseek-ai/dsh-client-connection có một thiết lập trustedHosts. README của plugin nêu quy tắc: header Host phải là loopback, địa chỉ bind của chính listener trên bất kỳ cổng nào, hoặc khớp với một mục trustedHosts. Khi browser gửi header Origin, header đó phải chỉ đúng cùng authority. Hàng rào này chạy trước bước xác thực trên mọi request và từ chối mọi request /api không vượt qua kiểm tra. Browser hiển thị lỗi từ chối như sau:
transport failure for /api/host.describe: HTTP 403Vì vậy, mọi proxy đặt phía trước DSH phải khai báo tên mà người dùng nhập vào browser. CLI nhận tùy chọn --trusted-host <authority> và có thể lặp lại tùy chọn này:
dsh web --trusted-host dsh.example.comHàng rào khớp theo authority, gồm host và port, đồng thời chuẩn hóa các port mặc định theo scheme của listener. Mục không có port như dsh.example.com sẽ khớp tên đó trên mọi port, nên cũng bao gồm dsh.example.com:8443. Mục có port như dsh.example.com:8443 chỉ khớp đúng host và port đó, không khớp trường hợp nào khác. Loopback không cần thêm mục, nên cả localhost:3080 và 127.0.0.1:3080 đều vượt qua. Lỗi 403 không giải thích được gần như luôn do tên trong thanh địa chỉ khác với các tên trong danh sách tin cậy. Lỗi 401 là vấn đề khác: hàng rào đã cho phép request đi qua, nhưng browser chưa có session cookie.
Hàng rào này là một kiểm tra origin. Nó không phải cơ chế xác thực. Bản thân browser không cho phép trang web thiết lập header Host, nên kiểm tra này ngăn một trang trên site khác điều khiển agent của bạn. Bất kỳ client nào không phải browser đều có thể tự do ghi header đó. Hãy xem trustedHosts là thiết lập tương thích cho proxy, không bao giờ xem nó là một cơ chế bảo mật.
Tùy chọn 1: local forward qua SSH
Hãy ưu tiên cách này vì nó không cần thêm gì trên server.
ssh -N -L 127.0.0.1:3080:127.0.0.1:3080 you@your-vps-L mở một listener trên laptop và chuyển tiếp từng kết nối đến 127.0.0.1:3080 theo địa chỉ được resolve trên VPS. -N có nghĩa là “không chạy remote command”, nên session chỉ mang forward và không làm gì khác. Việc ghi phía local là 127.0.0.1:3080 thay vì chỉ dùng 3080 là có chủ ý: nó buộc listener phía laptop chỉ bind vào loopback, để thiết lập GatewayPorts trong cấu hình SSH client không thể âm thầm publish lại agent ra network mà bạn đang kết nối.
Bây giờ mở launch URL mà DSH đã in ra, giữ nguyên cả token, trong browser trên laptop. Vì forward lắng nghe tại 127.0.0.1:3080, http://127.0.0.1:3080/?token=... được in ra sẽ hoạt động nguyên trạng. Chỉ dùng http://localhost:3080 là chưa đủ trong một browser session mới: nếu không có cookie, DSH sẽ trả về 401. Sau lần truy cập đầu tiên, cookie sẽ giúp bạn tiếp tục truy cập, nhưng cookie chỉ áp dụng cho đúng host. Vì vậy, hãy tiếp tục dùng hostname bạn đã mở đầu tiên, bởi cookie đặt cho 127.0.0.1 sẽ không được gửi đến localhost. Có 2 thứ hoạt động sẵn ở đây. Header Host xác nhận đây là loopback authority, nên vượt qua hàng rào /api mà không cần cấu hình gì. Browser cũng xem các origin loopback như http://127.0.0.1 và http://localhost là secure context. Điều này quan trọng vì web app DSH gọi crypto.randomUUID() trong lúc khởi động, mà browser chỉ cho phép hàm đó chạy qua HTTPS hoặc trên loopback origin.
Thêm -f để ssh tự chuyển xuống background sau khi forward hoạt động:
ssh -f -N -L 127.0.0.1:3080:127.0.0.1:3080 you@your-vpsĐánh đổi thực tế là: không có listener mới trên địa chỉ public và không cần thay đổi firewall rule, nên đây là mức tăng exposure nhỏ nhất có thể. Toàn bộ access control của bạn nằm trong cấu hình SSH. Vì vậy, SSH chỉ dùng key và tắt đăng nhập bằng password là điều kiện bắt buộc, không phải phần bổ sung tùy chọn. Đổi lại, tunnel chỉ thuộc về một client session. Tunnel sẽ chết khi laptop sleep, bạn phải tự khởi động lại và cách này không giúp bạn truy cập từ điện thoại.
Tùy chọn 2: Dùng Tailscale serve trên VPS
Tailscale tạo một mạng riêng giữa các thiết bị của bạn. Cài Tailscale trên VPS sẽ cấp cho máy đó một địa chỉ 100.x.y.z mà chỉ các thiết bị trong tailnet của bạn mới có thể định tuyến đến, còn các policy kiểm soát truy cập sẽ quyết định thiết bị nào được phép kết nối. Chia sẻ máy sẽ mở rộng quyền truy cập đó đến thiết bị của người dùng bên ngoài.
Chỉ tham gia mạng là chưa đủ, và đây là chỗ nhiều người bị mắc. DSH không lắng nghe trên địa chỉ 100.x.y.z vì nó đang lắng nghe trên loopback. Khi mở http://100.x.y.z:3080 trong trình duyệt, kết nối bị từ chối vì không có socket nào bind vào địa chỉ đó. --host có nhận địa chỉ cụ thể đó, nhưng bind vào địa chỉ không phải loopback sẽ phục vụ HTTP thuần và in ra cảnh báo, khiến vấn đề secure context từ Tùy chọn 1 xuất hiện lại.
tailscale serve là thành phần kết nối hai phần này. Nó chạy trên VPS, nhận lưu lượng từ mạng riêng của bạn rồi proxy đến một địa chỉ cục bộ:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
sudo tailscale serve --bg --https=443 localhost:3080
tailscale serve statustailscale serve status in ra URL theo dạng https://<machine>.<tailnet>.ts.net/. Hãy khởi động DSH với tên đó trong danh sách trusted, nếu không /api sẽ trả về 403 đúng như mô tả ở trên:
dsh web --public-url https://your-vps.your-tailnet.ts.net/ --trusted-host your-vps.your-tailnet.ts.net--public-url chỉ thay đổi nội dung DSH in ra, nên URL khởi chạy sẽ dùng tên trong tailnet và mở được ngay trên điện thoại. Bản thân nó không cấp quyền truy cập, vì vậy --trusted-host vẫn cần thiết. URL được in ra chứa credential còn hiệu lực, nên chỉ dán URL đó vào thiết bị của bạn.
Cách này cũng loại bỏ vấn đề secure context. Tailscale kết thúc kết nối HTTPS thực bằng certificate được cấp cho tên đó, nên crypto.randomUUID() khả dụng và UI có thể khởi động trong trình duyệt điện thoại. Không có gì đi đến public internet vì serve chỉ publish dịch vụ bên trong mạng riêng của bạn.
Không thay tailscale funnel vào đây. Đây là cùng họ command nhưng hướng đến public internet, và nó sẽ đưa một agent có quyền shell lên một hostname mà bất kỳ ai cũng có thể phân giải. Hãy đọc cách serve giữ lưu lượng bên trong tailnet còn funnel publish lưu lượng ra ngoài trước khi chạy một trong hai command này. Để xem phiên bản của toàn bộ thiết lập này dành cho điện thoại, hãy xem cách truy cập agent tự host từ điện thoại.
Đánh đổi là bạn sẽ có thêm một dependency. Mọi thiết bị cần UI đều phải tham gia mạng, và một coordination server mà bạn không tự vận hành sẽ quyết định thiết bị nào thuộc mạng đó. Nếu điều này không thể chấp nhận, Headscale control server tự host sử dụng cùng protocol trên hardware do bạn sở hữu.
Tùy chọn 3: reverse proxy TLS có xác thực
Dùng cách này khi trình duyệt không thể tham gia mạng riêng, chẳng hạn trên một máy bạn không quản trị. Bạn đang public một hostname ra Internet, nên lớp xác thực phải là xác thực thực sự và phải do proxy cung cấp. Launch token của DSH là một bearer credential nằm trong URL và được dùng chung bởi mọi người có token đó, nên không được tính là lớp xác thực này.
TLS là viết tắt của transport layer security, cơ chế mã hóa đứng sau https://. Trỏ nginx vào cổng loopback và đặt mật khẩu ở phía trước. Block map phải nằm trong context http, nên hãy đặt nó trong một file riêng dưới /etc/nginx/conf.d/:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}Tiếp theo là cấu hình cho site:
server {
listen 443 ssl;
server_name dsh.example.com;
ssl_certificate /etc/letsencrypt/live/dsh.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dsh.example.com/privkey.pem;
auth_basic "dsh";
auth_basic_user_file /etc/nginx/dsh.htpasswd;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cookie_flags ~ secure;
proxy_read_timeout 3600s;
proxy_buffering off;
}
}Tạo password file, kiểm tra cấu hình rồi reload:
sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/dsh.htpasswd you
sudo nginx -t && sudo systemctl reload nginxnginx -t phải in ra syntax is ok và test is successful. Nếu in ra lỗi, nginx vẫn tiếp tục phục vụ cấu hình cũ, nên hiện tại chưa có gì bị hỏng. Sau đó khởi động DSH với public name đã được trust, vì proxy_set_header Host $host forward dsh.example.com và fence sẽ từ chối nếu không có cấu hình này. --public-url khiến launch URL được in ra sử dụng name đó, để bạn có thể mở trực tiếp:
dsh web --public-url https://dsh.example.com/ --trusted-host dsh.example.comCó 3 directive trong đó rất quan trọng. Các header Upgrade và Connection giữ kết nối dài hạn của UI luôn mở. Nếu thiếu chúng, nginx sẽ đóng kết nối, khiến giao diện reconnect liên tục và hiển thị state cũ. proxy_read_timeout 3600s ngăn nginx ngắt một agent run mất hơn 60 giây mặc định để xử lý. proxy_cookie_flags ~ secure đánh dấu session cookie Secure của DSH trên kết nối HTTPS. Public deployment guide của DSH yêu cầu proxy thực hiện việc này, vì backend HTTP thuần sẽ phát hành cookie mà không có cờ đó. DSH cũng có thể tự phục vụ HTTPS bằng --tls-cert và --tls-key, nhưng không tự lấy hoặc gia hạn certificate và cũng không thêm password, nên không thay thế proxy trong trường hợp này. Phần giải thích lý do của các directive còn lại nằm trong giải thích cấu hình nginx reverse proxy theo từng directive, còn phần so sánh các proxy nằm trong so sánh nginx với Caddy và Traefik.
Cần hiểu rõ basic auth mang lại gì. Nó chặn các scanner quét ngẫu nhiên và tốt hơn nhiều so với việc để một cổng mở công khai. Tuy nhiên, đây vẫn chỉ là một password dùng chung đặt trước một shell, không có second factor và không có cách thu hồi quyền của một người cụ thể. Bất kỳ ai biết password và có launch URL hoặc session cookie đều có thể chạy command với quyền của user mà DSH đang chạy. Nếu có nhiều người cần truy cập, hãy đặt một identity proxy thực sự ở phía trước. Trước khi mở rộng thêm, hãy đọc các quy tắc an toàn khi chạy coding agent trên VPS.
Điều gì xảy ra nếu bind vào 0.0.0.0 và mở cổng
Cách tắt rõ ràng là bind lại và mở firewall. DSH chặn phần wildcard này. --host từ chối 0.0.0.0, còn plugin @deepseek-ai/dsh-host-webserver từ chối địa chỉ không xác định dưới mọi cách viết khi tải cấu hình, bao gồm 0.0.0.0, :: và các dạng IPv4-mapped. host chấp nhận một địa chỉ IPv4 hoặc IPv6 cụ thể ở dạng literal, vì vậy khai báo public IP là một thiết lập mà DSH sẽ chấp nhận. Có các hướng dẫn cộng đồng chỉ cách sửa package đã cài để loại bỏ kiểm tra wildcard. Các hướng dẫn đó có kèm cảnh báo, và những cảnh báo này là chính xác. Lớp plugin tương tự hữu ích hơn nhiều khi dùng theo hướng ngược lại: giới hạn chi tiêu và rule cấp quyền cho tool thu hẹp những gì agent được phép làm, thay vì mở rộng nơi agent lắng nghe.
Đây là những gì public bind thực sự mang lại. Cổng 3080 trả lời trên public IP của bạn qua HTTP thuần, và DSH in cảnh báo về việc này. Không có trang đăng nhập. Launch token là lớp bảo vệ duy nhất, và token này đi qua Internet dưới dạng cleartext trong URL; session cookie cũng vậy sau khi được tạo. Lớp bảo vệ /api kiểm tra header Host mà mọi client không phải browser tự ghi, và dù thế nào cũng cho phép bind address trên mọi cổng. Vì vậy, khai báo địa chỉ của bạn trong trustedHosts không thay đổi gì đối với attacker đang có curl. Deployment guide của DSH cũng nói rõ rằng cả advertised URL lẫn lớp bảo vệ này đều không bảo vệ chính listening port. Phía sau cổng đó là một agent chạy shell command bằng user của bạn, cùng với các credential trong $DSH_HOME, nơi DSH lưu profile và API key. Chỉ cần một token hoặc cookie bị sniff là attacker có thể thực thi code từ xa trên server của bạn, đồng thời phát sinh chi phí từ provider của bạn. Scanner liên tục quét toàn bộ address space, nên một IP không được liệt kê không phải là biện pháp kiểm soát. Mọi secret mà agent có thể đọc đều có thể bị đưa ra ngoài cùng shell.
Mọi phương thức ở trên đều tồn tại để bạn không bao giờ phải làm việc này. SSH forward là lựa chọn mặc định phù hợp khi một người dùng một laptop. Chuyển sang tailscale serve khi có điện thoại tham gia hoặc khi bạn muốn dùng certificate thực sự. Chỉ dùng public reverse proxy trong trường hợp browser mà bạn không quản trị cần truy cập UI, và phải đặt authentication thực sự ở phía trước.
FAQ
Vì sao http://127.0.0.1:3080 không mở được từ laptop của tôi?
Vì 127.0.0.1 có nghĩa là “máy đang đọc địa chỉ này”. DSH in địa chỉ đó trên VPS, nên địa chỉ này đúng ở phía VPS. Laptop của bạn đọc cùng chuỗi đó rồi truy cập interface loopback của chính nó, nơi không có tiến trình nào đang listening. Kiểm tra phía server bằng ss -ltnp | grep 3080 trên VPS. Dòng hiển thị 127.0.0.1:3080 nghĩa là DSH đang chạy và cố ý bind vào loopback. Bạn cần tunnel hoặc proxy, không phải một bind address khác, sau đó dùng đầy đủ launch URL cùng token của nó.
Có thể chạy dsh web với --host 0.0.0.0 không?
Không. Tài liệu CLI của repository, được kiểm tra vào ngày 11 October 2026, cho biết --host phải chỉ định một địa chỉ local cụ thể, không phải wildcard, và --host 0.0.0.0 sẽ thoát với lỗi sử dụng. Webserver plugin cũng từ chối địa chỉ này khi nạp cấu hình. Một địa chỉ cụ thể như public IP của bạn được chấp nhận, nhưng dịch vụ này dùng plain HTTP. Vì vậy launch token bảo vệ shell sẽ đi qua network dưới dạng cleartext. Các bản vá từ cộng đồng loại bỏ kiểm tra wildcard. Nếu áp dụng bản vá, bạn phải tự chịu trách nhiệm về firewall và transport security, vì DSH không cung cấp các cơ chế này.
Vì sao mọi lệnh gọi /api đều trả về HTTP 403 phía sau reverse proxy?
/api trust fence từ chối mọi request có header Host không phải là địa chỉ loopback, bind address của listener hoặc giá trị được liệt kê trong trustedHosts. Phía sau proxy, header đó chứa public name của bạn, nên trust fence từ chối request và browser ghi log transport failure for /api/host.describe: HTTP 403. Khởi động DSH với --trusted-host <your name> và đặt giá trị này khớp với tên hiển thị trên URL bar. Entry không có port sẽ khớp với tên đó trên mọi port. Entry có port phải khớp chính xác cả host lẫn port. Nếu các lệnh gọi trả về 401 thay vì 403, trust fence đã cho qua và browser chưa có session. Khi đó hãy mở launch URL được in ra, cùng token của nó.
Vì sao DSH UI tải được nhưng không bao giờ khởi động xong khi dùng plain HTTP?
Web app gọi crypto.randomUUID() trong quá trình boot. Browser chỉ cung cấp function này trong secure context: HTTPS hoặc loopback origin như http://localhost. Khi được phục vụ qua plain HTTP từ một bare IP address, function này không được định nghĩa. Các lệnh gọi phụ thuộc vào nó sẽ throw, nên interface không bao giờ tải đầy đủ. Phục vụ UI qua HTTPS bằng tailscale serve hoặc qua TLS reverse proxy sẽ giải quyết vấn đề. Truy cập UI qua SSH forward trên http://localhost cũng có tác dụng tương tự.
Một developer nên dùng phương thức nào?
SSH local forward. Phương thức này không thêm gì vào server, không thay đổi firewall rule và dùng lại SSH key mà bạn đã tin cậy. Chạy ssh -f -N -L 127.0.0.1:3080:127.0.0.1:3080 you@your-vps, sau đó mở launch URL mà DSH đã in ra, http://127.0.0.1:3080/?token=..., với token còn nguyên. Chuyển sang tailscale serve khi bạn muốn mở UI trên điện thoại hoặc muốn quyền truy cập vẫn hoạt động sau khi laptop chuyển sang chế độ sleep.