DeepSeek Harness on a VPS: keep it private
Install the DeepSeek Harness on a Linux VPS, pin the npm version, understand what a plugin can do, and reach the port 3080 web UI over an SSH tunnel.
What the DeepSeek Harness is
The DeepSeek Harness (dsh) is a Node.js agent runtime you can run on a VPS (virtual private server), and the safe way to run it is bound to 127.0.0.1 with your browser reaching it through an SSH (secure shell) tunnel. It serves a web UI (user interface) on port 3080 instead of living in a terminal. That web server asks for no password of its own, so a published port 3080 hands anyone who finds it an agent that reads your files and runs commands as your Linux user.
DeepSeek released it on 13 August 2026 under the MIT licence, as the npm package @deepseek-ai/dsh. The project describes itself as a developer preview and states that compatibility-breaking changes are expected. Every version number below is a snapshot of August 2026, so check the repository before you copy any of it onto a box that matters.
One idea runs through the whole design: everything is a plugin. The model adapter, the tool registry, the session log, the sandbox, the scheduler and the agent loop itself are plugins loaded into one shared context, and any of them can be replaced. There is no privileged core that plugins merely decorate. That is what makes the harness worth trying, and it is also where the one real risk lives.
A harness is not a model
The harness runs the agent loop. The reasoning happens in a model somewhere else, so nothing works until you give it either an API (application programming interface) key or the address of a model endpoint you host yourself.
You configure that in the UI under Settings and then Models. The catalog has ready-made cards for the big API providers (DeepSeek, OpenAI, Anthropic) where you paste a key. "Add a custom provider" is the interesting option: it takes a provider ID, a display name, a base URL, an API protocol and a credential, and it speaks the OpenAI-compatible protocol, so any gateway or local server that implements that protocol works. Custom providers can also query the OpenAI-compatible GET /models endpoint to fill in the model list for you.
That is how you point the harness at a model on the same VPS. Ollama exposes an OpenAI-compatible API at http://127.0.0.1:11434/v1/, and it wants the API key field filled with any string, ollama by convention, because the field is required and then ignored. Whether a model small enough to fit your VPS is good enough to drive an agent is the harder question, and the difference between Ollama and vLLM as a local model server decides how much of your RAM the answer costs.
Keys typed into the UI are write-only. The harness stores them in $DSH_HOME/.credentials.yaml and keeps only a credential reference in settings.yaml. $DSH_HOME defaults to ~/.dsh. Treat that file as a password file, because it is one: anyone who reads it can spend your API budget.
What you need before you install
- a VPS running Ubuntu 24.04 or another current Linux, with SSH access
- Node.js 22.19 or newer in the 22.x line, or Node.js 24 and above, which is what the project builds and tests against
- a normal user account, not
root, because the agent runs shell commands as whoever started the process pnpmon the PATH if you plan to install plugins, since the plugin command shells out to it- port 3080 closed on your firewall and on your provider's separate network firewall
Ubuntu's own nodejs package is older than the harness needs, so install Node from NodeSource or nvm rather than reaching for apt install nodejs. If the VPS is fresh, hardening SSH before anything else is worth ten minutes, because the tunnel you are about to depend on is only as good as the SSH server behind it.
Install the DeepSeek Harness on a VPS, pinned to one version
node --version
npx @deepseek-ai/dsh@0.1.0-rc.6 webnpx downloads the package and runs its dsh binary. web is an alias for --profile web, which boots the browser application, and the process prints the address it is listening on. The default is http://127.0.0.1:3080.
Pin the version. npx @deepseek-ai/dsh web resolves whatever the latest tag points at the minute you run it, and the project has already shipped several release candidates and says breaking changes are coming. 0.1.0-rc.6 is what latest pointed at on 13 August 2026. A pinned version means the box you set up today behaves the same next month, so an upgrade becomes a decision you make instead of an accident you discover.
For daily use, install it once instead of re-resolving on every start.
npm install -g @deepseek-ai/dsh@0.1.0-rc.6
dsh --profile web --helpThat second line is worth running, because the launcher and the web app carry separate flag sets. dsh --help shows the launcher's own options. dsh --profile web --help shows the flags the web application accepts, which is where --port, --host and the repeatable --trusted-host live.
Now confirm what it is listening on.
ss -tlnp | grep 3080The local address column should read 127.0.0.1:3080. If it reads 0.0.0.0:3080, the UI is reachable from the internet, and you should stop the process before you do anything else.
Why you must never publish port 3080
The web server has no authentication layer. Its configuration exposes a listen host and a listen port, and that is the whole surface. Access control for non-loopback deployments is a separate trusted-host setting, which is not a login screen.
Now add what sits behind that port. The agent edits files in the workspace and runs shell commands, and your provider credentials sit on disk next to it. So an open port 3080 is a remote shell with a chat interface, running as the user that started it, with your API key attached. Nobody needs an exploit for that. They need the port number, and scanners find port numbers within hours of a host coming online.
The CLI (command line interface) agrees with this. As of 0.1.0-rc.6 it deliberately does not support --host 0.0.0.0 and exits with a usage error instead of starting. That refusal is a feature, so do not go looking for a patch that removes it.
Two other deployments are reasonable when a tunnel does not suit you. Put the box on a private overlay network so it holds an address only your own devices can route to, which is what a self-hosted Headscale control server gives you. Or front it with a reverse proxy that authenticates the request before it reaches port 3080, for example an Authentik single sign-on server doing forward auth. A reverse proxy with no auth in front of it is not a security control. It is a longer URL.
Reach the web UI over an SSH tunnel
Run this on your laptop, not on the server.
ssh -N -L 3080:127.0.0.1:3080 you@your-server-L opens port 3080 on your laptop and forwards anything that connects to it through the encrypted SSH session. The 127.0.0.1:3080 part is resolved on the server, so the connection arrives at the harness from loopback, exactly as if you were sitting at the machine. -N says do not start a remote shell, because you want the forward and nothing else.
Then open http://127.0.0.1:3080 in your local browser. If port 3080 is already busy on your laptop, change the left-hand number: ssh -N -L 3180:127.0.0.1:3080 you@your-server, then browse to http://127.0.0.1:3180. The number on the left is local and the number on the right belongs to the server, so only the left one moves.
Save it in ~/.ssh/config and stop typing it.
Host dsh
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
LocalForward 3080 127.0.0.1:3080After that, ssh -N dsh starts the tunnel. A browser reporting that the connection was refused usually means the tunnel is up but nothing is listening on the far side, because SSH forwards the port whether or not the harness is running. Check the server with the ss command above.
Keep the harness running after you log out
An npx command dies with your shell. A systemd user service survives, and it brings the harness back after a crash or a reboot.
loginctl enable-linger $USER
mkdir -p ~/.config/systemd/user
command -v dshenable-linger matters because user services normally stop when your last session ends, so without it the harness dies the moment you close the tunnel. Take the absolute path printed by command -v dsh and put it in the unit, because systemd does not search the PATH your login shell builds.
[Unit]
Description=DeepSeek Harness web UI
After=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/projects/site
ExecStart=/usr/local/bin/dsh web
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.targetWorkingDirectory is not cosmetic. The dsh process uses its invoking directory as the default filesystem location, so a service started in the wrong place gives the agent the wrong default workspace. You can still pick the workspace in the UI.
systemctl --user daemon-reload
systemctl --user enable --now dsh
systemctl --user status dshA unit that refuses to start is almost always a wrong ExecStart path or a Node version the binary rejects, and journalctl --user -u dsh -n 50 names which. The same pattern covers keeping any coding agent alive on a VPS, and the failure modes are identical.
What a plugin is allowed to do
A plugin is a module that contributes services, typed events and reversible effects to a shared context. The extension points are the part worth reading closely:
- register a model provider on
ctx.llm - add model-facing tools on
ctx.tools - provide the shell backend behind
ctx.shell - provide filesystem access or policy behind
ctx.fs - register human commands on
ctx.commands - run background work through
ctx.jobs - wrap spawned processes with a
ctx.sandboxbackend - intercept requests and tool calls through the
agent/*andtools/*events - extend durable session state
- drive the UI through
ctx.agents
Read that list the way an attacker would. A plugin can supply the filesystem layer and the shell layer, and it can sit in the middle of every tool call the model makes. No permission dialog stands between a plugin and those seams, because a plugin is ordinary Node code loaded into the same process as everything else. Installing a plugin is running a stranger's code with your agent's permissions, and your agent's permissions are your Unix user's permissions.
This is the same trust decision you make when attaching an MCP server to an agent on a VPS, where MCP is the model context protocol. It is also why running a coding agent safely on a VPS starts with the account it runs under rather than with the model, and why npm supply chain attacks land so hard on servers: the install step is the compromise, and nothing prompts you.
Where plugins come from
Plugins live in profiles. A profile is a named composition stored under $DSH_HOME, which defaults to ~/.dsh, and each profile directory holds the out-of-tree plugins it installs. The CLI manages them by forwarding your arguments straight to pnpm, using the profile directory as the working directory.
dsh plugin --profile web add github:deepseek-harness/turtle-ui
dsh plugin --profile web remove turtle-uiBecause the arguments reach pnpm unchanged, add, remove, update and why behave the way they do in any pnpm project, and a plugin can be an npm package or a GitHub reference. pnpm must exist on the PATH first. On Node 22 and later, corepack enable pnpm puts it there.
Discovery works through a GitHub topic. Plugin authors add the dsh-plugin topic to their repository, and browsing that topic is how you find what exists. A topic is a label an author applies to their own repository. Nobody reviews it and nobody signs it, and the topic page ranks by stars, which measures popularity rather than safety.
Four habits keep this manageable. Read the source before installing, since most plugins are small enough to read in ten minutes. Pin the exact version or commit instead of tracking a branch. Run the harness under a user that owns nothing else, on a VPS you would be willing to rebuild. Give the agent its own API key with its own spending limit, separate from the key your production services use.
If you would rather compare designs before committing to one, the Omnigent multi-agent harness answers the same problem with a different structure, and the trade-offs become obvious once plugins are in play.
What breaks first
Node is too old. The project targets Node 22.19 and newer in the 22.x line, or Node 24 and above, and its CI tests those. An older runtime fails at startup because the code uses syntax and APIs it does not have. Run node --version before anything else.
Port 3080 is already taken. A second harness, a stale process, or an unrelated application that also likes 3080. Find it with ss -tlnp | grep 3080, then stop it or start the harness elsewhere with dsh web --port 3180. --port belongs to the web app, so it goes after web.
The browser cannot connect through the tunnel. Confirm you browsed to 127.0.0.1 and not the server's public address, because the forwarded port exists only on your laptop. Then confirm the harness is listening on the server, since SSH sets up the forward whether or not anything answers on the far end.
dsh plugin fails immediately. The command is a wrapper around pnpm, so a missing pnpm binary stops it before any plugin work begins.
The agent cannot see your project. The workspace defaults to the directory the process started in, so a unit whose WorkingDirectory is your home directory gives the agent your home directory. Choose the workspace in the UI, or fix the unit and reload it.
FAQ
Is it safe to expose the DeepSeek Harness web UI on port 3080?
No. The web server has no login of its own, and the agent behind it edits files and runs shell commands as the user that started the process, with your provider API key stored on the same disk. Keep the listener on 127.0.0.1 and reach it over an SSH tunnel. A private overlay network, or a reverse proxy that authenticates every request before it reaches the port, also works. As of version 0.1.0-rc.6 the CLI refuses --host 0.0.0.0 and exits with a usage error, which tells you what the authors think of the idea.
Do I need a DeepSeek API key, or can I use a local model?
Either works, because the harness is a runtime and not a model. Under Settings and then Models you can paste a key into a catalog provider card, or choose "Add a custom provider" and give it a base URL that speaks the OpenAI-compatible protocol. A local Ollama server answers at http://127.0.0.1:11434/v1/ and accepts any string in the API key field. Keys land in $DSH_HOME/.credentials.yaml, which defaults to ~/.dsh/.credentials.yaml.
What does installing a DeepSeek Harness plugin actually give the plugin?
The permissions of the account running the harness. A plugin is Node code loaded into the same process, and the extension points include the shell backend, the filesystem layer, the tool registry and the events wrapping every tool call. Nothing sandboxes a plugin away from those seams unless a plugin supplies the sandbox itself. Read the source before you install, and run the harness as a user that owns nothing you care about.
Which version should I install, and will it keep working?
Install an exact version, for example npx @deepseek-ai/dsh@0.1.0-rc.6 web. That is what the latest tag pointed at on 13 August 2026. The project calls itself a developer preview and says compatibility-breaking changes are expected, so an unpinned command can behave differently from one day to the next. Check the repository before upgrading, and expect config keys and plugin interfaces to move while the version still starts with 0.