SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

Why dsh dey show http://127.0.0.1:3080

dsh dey print http://127.0.0.1:3080 because Web UI bind to localhost only. Use SSH tunnel reach am safely, and no publish port 3080 anyhow.

Wetin dsh web: http://127.0.0.1:3080 mean

When you start the DeepSeek Harness web profile for VPS, e go print line like this, then wait:

dsh web: http://127.0.0.1:3080/?token=...

127.0.0.1 na loopback address. Na the address machine dey use to talk to itself. Socket wey bind to 127.0.0.1 go accept connection from process wey dey run for that same machine, and nowhere else. So that line dey tell you two things at once: where Web UI dey listen, and who fit reach am. Na only the machine wey dsh dey run on fit reach am. The ?token= part for the end na fresh random credential wey the process dey create every time e start. We go explain more on that below.

Na why the URL no do anything when you paste am for browser on your laptop. Your laptop's 127.0.0.1 na your laptop. The harness dey listen on the VPS's 127.0.0.1, and that one dey belong to different machine with different loopback stack. Nothing spoil. You need carry the connection cross go there.

The official README talk the default plainly: "The command starts the Web UI at http://127.0.0.1:3080 by default and opens it in the default browser for a local launch." When you use SSH, browser no dey open, so the URL remain for you to use by hand. The bind address come from the webserver host plugin, @deepseek-ai/dsh-host-webserver. For the 0.2.0-rc.2 release wey this guide use, the host key dey documented as "Listen host; the two supported values are loopback and all-interfaces". You go get loopback unless you change am. If ports still new to you, how ports dey work for Linux explain the address-plus-port model wey everything here dey based on.

Why Web UI dey bind to localhost only

dsh na an agent harness, wey be the program wey dey wrap the model: na e dey control the loop, tool calls, and the permissions wey dem calls dey run with. Browser tab na control surface for process wey dey run shell commands, read and write files for the workspace directory wey you choose, and spend your model API key. Anybody wey fit load that page fit do all those things as the user wey dey run dsh. Network no be the only way to reach those permissions: plugin wey you install dey run inside the same process with the same permissions. Na why you need check dsh plugin well before you install am with the same care wey you go use decide wetin server go listen on.

So port 3080 no be read-only dashboard. If person load that page, e get command execution for the server.

Open the URL wey e print, and you go enter session list directly. No login screen dey, and no user accounts dey. Na token for the end of the URL dey protect the page: root page go exchange am for signed session cookie, then redirect you go clean address. After that, every RPC call and WebSocket stream wey UI make without valid cookie go get 401 Unauthorized. But no be everything for the port dey protected. Connection plugin documentation talk say static assets remain public, and webserver itself no get server-wide authentication or origin policy. So every plugin wey own route go enforce its own rules. For those protected routes, Connection plugin also return 403 when Host header no be loopback or host wey you explicitly trust. For loopback, this na sensible second layer behind the real access control, because operating system dey allow only local processes pass. E no be reason to publish the port. Token dey your terminal scrollback and for any log wey capture startup output. The project's own reverse proxy guide talk say neither its Host check nor the advertised URL protect the listening port itself. Webserver documentation warn say binding non-loopback address "still exposes unprotected routes and static assets to that network". And for 0.2.0-rc.2, webserver plugin no get TLS by itself. Automated scanners dey scan uncommon ports continuously, so treat published 3080 as port wey dem don find.

No open port 3080 for your firewall, and no set webserver host to 0.0.0.0, or to your public IP, for public VPS. That combination leave command execution on your server just one leaked URL away from anybody wey connect.

Current releases already dey push you follow this approach: web application's own --host flag refuse to bind all interfaces. But for 0.2.0-rc.2, webserver plugin's host config key still accept 0.0.0.0, so config edit fit still expose am. Webserver documentation for project's main branch go further: e reject unspecified address when e load and add optional tls setting. E still accept any one concrete address for local interface, so if you point host to your VPS public IP, you still expose the UI.

The same reasoning apply to every agent runtime wey you put for server. Na why to run coding agent safely for VPS start with the same rule: agent control port stay private, and trusted thing go carry you reach am.

How I fit open dsh Web UI from my laptop?

Three correct ways dey, and every one of dem go leave the harness bound to loopback.

  • SSH tunnel. Nothing new go listen for public interface, and you already get the credentials. Na this one you suppose use.
  • Private overlay network, so UI fit dey reachable from your own devices and remain invisible to everybody else.
  • Reverse proxy wey go terminate TLS (transport layer security) and demand password before e forward anything.

The difference na wetin carry your browser go loopback. None of dem suppose involve moving the harness comot from loopback.

Alcan am with SSH tunnel

Run dis one for your laptop, no be for VPS:

ssh -N -L 3080:127.0.0.1:3080 you@your-vps

Leave am make e dey run, then open the complete URL dsh wey e print, http://127.0.0.1:3080/?token=... with the token inside, for your local browser. Web UI go load, exchange the token for session cookie, then remove the token from address bar. Your browser go send 127.0.0.1:3080 as the Host, and na loopback be that, so tunnel go pass Host check without extra configuration.

The -L argument get three fields wey colon separate. The first one na the port to open for your laptop. The second and third na the address and port wey dem go forward each connection go. The important detail be say: 127.0.0.1 for the middle field na SSH server for VPS go resolve am, after your traffic don reach there. E mean VPS loopback, no be your own. Na exactly the address wey dsh print, and na why tunnel dey work when direct browser connection no dey work.

-N tell SSH make e no run remote command, so you go get forwarder without shell. For background tunnel wey go show error clearly instead of failing quietly:

ssh -N -f -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -L 3080:127.0.0.1:3080 you@your-vps

-f go put am for background after authentication. ExitOnForwardFailure=yes matter pass as e look: without am, SSH fit connect successfully even when e no fit set up the forward, so you go get working session but dead tunnel without warning. ServerAliveInterval=30 go send keepalive every 30 seconds, so idle tunnel fit survive NAT (network address translation) timeout for café and hotel routers.

Wetin you suppose see

For VPS, confirm wetin dey listen:

ss -ltnp | grep 3080

Healthy result go name the loopback address:

LISTEN 0  511  127.0.0.1:3080  0.0.0.0:*  users:(("node",pid=1042,fd=21))

If local address column show 0.0.0.0:3080 instead, Web UI dey every interface, including the public one. If your VPS public IP dey that column, na the same exposure. Stop am and fix the bind before you do anything else. If ss print the socket but leave users: field empty, run am with sudo, because process name for socket wey another user own dey hidden otherwise.

When tunnel refuse to start

SSH go print dis and exit:

bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080

Na your laptop concern, no be server. Something local don already hold port 3080, often na old tunnel wey you forget. Pick another free local port:

ssh -N -L 3081:127.0.0.1:3080 you@your-vps

Na only the first field change, so now browse go http://127.0.0.1:3081/?token=..., with the same token, while harness still dey listen on 3080. The two numbers no need match. Session cookie dey bound to host and port for address bar, so new local port need the token URL one more time.

If tunnel come up but browser report refused connection or empty reply, traffic don cross go VPS but e no find anything for the far end. Either dsh don exit, or e bind different port. Check with ss -ltnp | grep 3080 for server. If page instead answer 401 Unauthorized, tunnel dey okay and your browser simply no get valid cookie for that address: open the printed URL again, with token inside. After restart, collect URL from the new output, because each process dey mint fresh token.

One more thing dey catch people here. Foreground npx @deepseek-ai/dsh web go die when its shell close, so harness go stop immediately you log out. Start am inside tmux or under a systemd user service, wey na the same problem solved for keeping coding agent dey run for VPS. Under systemd, the dsh web: line, token and everything, go enter service log, so read am from there. While you dey work on SSH side, hardening SSH for your VPS dey worth doing first, because tunnel make your SSH login the only door to the agent.

Reach am through a private overlay network

Overlay network dey give your VPS and laptop addresses for private network wey na only your devices fit join. Tailscale na common choice, and e serve command fit this case well: tailscaled dey run for the VPS and e connect to localhost:3080 by itself, so the harness remain for loopback.

tailscale serve --bg localhost:3080
tailscale serve status

You fit reach the UI with your machine name inside your tailnet, through HTTPS, without opening any port for the public interface. This one need HTTPS certificates make e work for your tailnet. Otherwise, serve no get certificate to present. Since your browser now dey send that tailnet name as the Host, start the harness with that name as trusted. If you no do am, its API calls go get 403. Entry wey no get port go match any port:

npx @deepseek-ai/dsh@0.2.0-rc.2 web --trusted-host your-vps.your-tailnet.ts.net

Then open https://your-vps.your-tailnet.ts.net/?token=..., and carry the token from the printed line. To bring am down again, repeat the command with off:

tailscale serve --https=443 off

Use serve, never funnel. Funnel dey publish the same target to the public internet. This one expose an agent runtime for open port, with only URL token in front of am. The two commands dey look almost the same, but dem dey do opposite things. So read the difference between Tailscale Serve and Funnel before you type either one. Tailscale as a private network explain the setup itself.

Reach am through reverse proxy wey dey check password

Na this option really dey publish port to internet, so na the authentication alone dey between stranger and command execution for your server. Choose am when several people need the UI and e no practical make everybody get separate tunnel.

The harness dey on 127.0.0.1:3080. nginx dey run for the same box, so e fit reach loopback, and e dey listen on 443 with certificate and password file.

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 Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
        proxy_buffering off;
    }
}

Create the password file and reload:

sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/dsh.htpasswd you
sudo nginx -t && sudo systemctl reload nginx

nginx -t suppose print syntax is ok followed by test is successful. If file spoil, reload go fail and leave the running configuration as e be, so read the error instead of restarting anyhow.

Three of those proxy lines no be decoration. The Upgrade and Connection headers allow the WebSocket handshake pass, and without dem the page go load but e no go ever update. proxy_read_timeout 3600s replaces the 60 second default. Otherwise e go cut long agent run midway through response and make the UI look frozen. proxy_buffering off sends model output go browser as e dey arrive instead of holding am until response complete. nginx reverse proxy config, line by line explains the remaining parts, and how to choose between nginx, Caddy and Traefik covers how to do the same thing with automatic certificates.

The harness sef needs to know about the proxy. proxy_set_header Host $host passes the browser-facing name through unchanged. Na wetin the project proxy guide request, so start dsh with that name trusted, or e go answer 403:

npx @deepseek-ai/dsh@0.2.0-rc.2 web --trusted-host dsh.example.com

Each person go open https://dsh.example.com/?token=... once, with the token from the dsh web: line, and the browser go keep the session cookie after that. The backend sets that cookie without the Secure flag, and the proxy guide asks the proxy to add am for HTTPS. For nginx 1.19.3 or newer, proxy_cookie_flags ~ secure; inside the location block go do that.

Keep 3080 closed for the firewall no matter which proxy you choose, so the only way enter na through the authenticated one. ufw firewall basics explains the rules. Basic authentication over TLS na only the minimum; e no be complete security model. Anybody wey get that password and the token URL get shell access to your server. Prefer the tunnel whenever you fit.

How I fit change di port wey dsh web dey listen on?

--port belong to di web application, e no belong to di launcher. Di CLI documentation give di example directly:

dsh --profile web --port 8080

dsh web na alias of --profile web, so dsh web --port 8080 na di same command. Di launcher dey parse only im own flags and hand everything after dem to di profile wey e boot. So launcher flags go first, and di first token wey launcher no recognise go start di application arguments. Put --port after di profile, never before am.

Read di URL wey di command print instead of assuming am, because dat line show di address wey di server actually bind to, together with di token wey you go need. Then update di last field of your tunnel to match:

ssh -N -L 3080:127.0.0.1:8080 you@your-vps

For permanent change, di port dey inside di profile configuration instead of di command line. Di web and headless profiles dey auto-initialise for first use from shipped templates, under ~/.dsh. Na for dat same directory your API key and model endpoint settings dey, so configure dsh keys, models and endpoints na di companion read wey you fit use once you dey edit these files anyway. To see wetin actually dey active after all di layers don compose:

dsh --dump-config

Di listen address dey come from two keys for di webserver plugin, host and port. For 0.2.0-rc.2, im other keys only dey tune response compression, and di main branch add optional tls key. Setting port to 0 tell di operating system make e choose free port, as documentation talk: "zero requests an OS-assigned port". Dis guarantee say conflict no go happen, but e no good for tunnel, because di number go change every time you restart.

Why dsh dey fail with address already in use?

Na because another process don already hold that address and port, so kernel no go allow the second bind. Node dey report am like this:

Error: listen EADDRINUSE: address already in use 127.0.0.1:3080

Find the process wey dey hold am before you change anything:

sudo ss -ltnp | grep 3080

The users:(("node",pid=1042,fd=21)) field show the process and im PID. Usually, na an earlier dsh wey you think say don stop, but e still dey alive inside detached tmux window. Stop that one with kill 1042, or start the new instance for another port. Note say 127.0.0.1:3080 and 0.0.0.0:3080 still go collide with each other, because binding all interfaces already cover loopback.

Pin the version, because this one na developer preview

The README talk am plainly: DeepSeek Harness dey developer preview and dey change fast, and compatibility fit break with new changes. If na this speed make you dey hesitate, how dsh compare with Claude Code and Omnigent dey compare am with two harnesses wey dey different points for the same curve.

An unpinned npx @deepseek-ai/dsh web no specify any version, so you no control which build e go start. npm documentation talk say package name wey no get specifier “go match any version wey dey for the local project”, and package wey no dey for the project go install inside one folder for npm cache. Which build go start depend on wetin dey that machine, so restart fit bring up different CLI from the one you test, with different flags. Pin the version so restart no turn upgrade:

npx @deepseek-ai/dsh@0.2.0-rc.2 web

As of October 2026 (checked on 2026-10-11), npm latest tag dey point to 0.2.0-rc.2, wey dem publish on 2026-09-29, and one next tag dey point to the same build. Another alpha tag get 0.2.1-alpha.2, wey dem publish on 2026-10-09. The 0.1.0-rc.7 build wey this guide pin for August don dey many preview releases behind now, across the remaining 0.1 line and enter 0.2. Check wetin bare npx go pull before you accept am:

npm view @deepseek-ai/dsh version

npm view @deepseek-ai/dsh dist-tags dey list all tags together. Na so you fit know release candidate from alpha before you pin am.

If the pin no gree install, or npx still dey start the old build after you pin new one, the install and version errors wey this one throw explain how to clear npx cache and check which npm your Node package come with.

Flags dey move between the launcher and the web application across preview releases. If --port stop to behave as this guide describe, ask the application for its own flag list instead of guessing:

dsh --profile web --help

For the install, workspace setup, and model key, see installing DeepSeek Harness on a VPS. For shorter walkthrough of only the access step, reaching the dsh Web UI on a VPS cover the tunnel without the reasoning.

FAQ

Why I no fit open http://127.0.0.1:3080 for my laptop browser?

Because 127.0.0.1 mean the machine wey you dey type on. The DeepSeek Harness Web UI dey bound to the VPS loopback address, so na only processes for the VPS fit connect to am. Your laptop get its own separate loopback, and nothing dey listen on port 3080 there. Forward the port through SSH with ssh -N -L 3080:127.0.0.1:3080 you@your-vps, then load the complete URL wey e print, http://127.0.0.1:3080/?token=..., locally. The middle field for the -L argument dey resolve for the server side, and na wetin make am point to the harness. If you leave the token out, you go get 401 Unauthorized until your browser hold session cookie.

E safe to bind dsh Web UI to 0.0.0.0 for public VPS?

No. Web UI na the control surface for an agent wey dey run shell commands and edit files as the user wey dey run dsh. E no get user accounts or login screen: the only credential na the token for the printed URL, static assets dey serve without am, and the project documentation itself talk say the listening port no get protection. If you bind am to all interfaces for public IP, anybody wey fit reach port 3080 fit run commands for your server once dem leak the URL. Binding am directly to public IP get the same risk. Keep the bind for 127.0.0.1, keep 3080 closed for firewall, and use SSH tunnel, private overlay network, or reverse proxy wey require password.

How I fit keep dsh Web UI running after I close my SSH session?

Foreground npx @deepseek-ai/dsh web na child of your login shell, so e go die when that shell exit. Start am inside tmux session and detach with Ctrl-b d, or run am as systemd user service with lingering enabled. The tunnel and the harness dey independent: you fit drop and rebuild the SSH tunnel anytime without touching the running harness, as long as the harness itself get parent wey go outlive your login.

Why dsh Web UI dey freeze for middle of long agent run behind nginx?

Because nginx default proxy_read_timeout na 60 seconds, so e dey close connection wey no produce data for one minute. Long agent step fit easily do this. Set proxy_read_timeout 3600s; inside the location block. Add proxy_buffering off; so output go stream to browser as e dey arrive, and pass Upgrade and Connection headers with proxy_http_version 1.1; so WebSocket handshake fit succeed. Without those headers, page go load but e no go ever receive update.