Deploy DeepSeek Harness on Ubuntu: Installation, Nginx Reverse Proxy, Credentials, and WeChat Integration
DeepSeek Harness can be deployed on an Ubuntu cloud server and exposed through Nginx while keeping the Harness web service bound to localhost. During deployment, the main issues are usually Node.js and pnpm versions, DSH profile configuration, credentials format, plugin compatibility, and reverse-proxy settings.
This guide documents a practical deployment process, including the errors encountered during installation, how they were diagnosed, and how the WeChat integration was changed from the incompatible deepseek-harness-weixin plugin to dsh-clawbot.
1. What This Deployment Is Trying to Achieve
The target architecture is straightforward.
Browser
│
▼
Nginx :80
│
▼
127.0.0.1:3080
│
▼
DeepSeek Harness Web
│
├── DeepSeek API
│
└── WeChat / ClawBot integration
The DSH web service listens only on localhost.
Nginx handles external HTTP access and forwards requests to DSH. This avoids exposing the DSH process directly to the public network.
The important components are:
2. Install Node.js Before Installing DeepSeek Harness
The first prerequisite is a working Node.js environment.
If the server returns
Command 'node' not found, but can be installed with:
apt install nodejs
Node.js has not yet been installed or is not available in the current PATH.
After installation, verify the runtime.
node -v
npm -v
pnpm -v
The deployment used Node.js 22.23.2 and pnpm 10.33.4.
A typical environment check looks like this.
node -v
npm -v
pnpm -v
The important point is to establish the runtime first. DSH plugin compatibility depends heavily on the installed DSH and Node.js versions.
3. Install DeepSeek Harness Globally
After Node.js and pnpm are ready, install DSH globally.
The installed DSH version in this deployment was:
0.1.5-rc.1
Check the installation with:
dsh --version
Also verify that the executable can be located.
which dsh
On a global npm installation, the package and executable may be located under the global Node.js installation directory.
4. Create and Inspect the Web Profile
The DSH web profile is stored under:
/root/.dsh/profiles/web
The profile contains its own package.json.
A simplified profile configuration looks like this.
{
"name": "dsh-profile-web",
"private": true,
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app"
],
"patchReload": "live"
}
}
}
One of the most important troubleshooting lessons was that removing a plugin from dependencies does not necessarily remove it from the profile.
If a plugin remains in the bundles array, DSH can continue loading it.
For example, this configuration still loads the old WeChat plugin.
{
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"deepseek-harness-weixin"
]
}
}
}
If the plugin has been removed, its entry must also be removed from bundles.
5. A Broken package.json Can Prevent DSH from Starting
After manually editing the profile, DSH initially failed with a JSON parsing error similar to:
SyntaxError: Unexpected token ']', ...
is not valid JSON
This indicates that the problem occurs before the application itself starts.
The first thing to check is the profile’s JSON syntax.
cd /root/.dsh/profiles/web
cat package.json
Then validate the JSON with Node.js.
node -e "JSON.parse(require('fs').readFileSync('package.json','utf8')); console.log('package.json OK')"
If the file contains an extra comma, unmatched bracket, or malformed object, DSH will fail before loading the application.
This is particularly easy to trigger when manually deleting a plugin entry.
6. The DSH Credentials File Must Use the Correct YAML Structure
Once the profile JSON was fixed, DSH progressed further but stopped while reading credentials.
The credentials file was:
/root/.dsh/.credentials.yaml
The relevant error indicated a problem with the version field.
The installed credentials package was checked directly.
node -e "console.log(require('/usr/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-credentials-local/package.json').version)"
The installed version was:
0.1.5-rc.2
Its source showed that the document version is expected to be the numeric value:
version: 1
rather than:
version: "1"
This distinction matters because YAML treats the two values differently.
A valid basic structure is:
version: 1
records:
client-connection/browser-session:
kind: grant
payload:
version: 1
secret: <REDACTED>
Do not paste browser-session secrets into public logs, tickets, articles, or chat messages.
7. Configure the DeepSeek API Key Without Printing It
The API key should be entered interactively so it does not appear in the shell history or command line.
Use:
read -rsp "Enter your new DeepSeek API Key: " NEW_KEY
echo
The terminal will appear to accept no input while typing.
That behavior is intentional because read -s disables character echo.
You can verify that a value was entered without displaying the key.
[ -n "$NEW_KEY" ] && echo "API Key entered" || echo "API Key is empty"
If the result is:
API Key entered
the variable contains a value.
The credentials structure can then contain a reference such as:
refs:
DEEPSEEK_API_KEY: "<REDACTED>"
The actual key should never be included in documentation.
8. Why the Original WeChat Plugin Failed
The original deployment used the deepseek-harness-weixin plugin.
The DSH process eventually failed with:
The requested module '@deepseek-ai/dsh-agent-presets'
does not provide an export named 'resolveSessionPreset'
This error is important because it identifies a package API compatibility problem.
The installed @deepseek-ai/dsh-agent-presets package exported functions including:
AgentPresetSettingsSchema
AgentPresets
SHIPPED_PRESET_ROOT
agentPresetProjectionDefinition
discoverPresets
livePresetMounts
mountPreset
readPresetMetadata
renderPresetMetadata
It did not export:
resolveSessionPreset
The old WeChat plugin expected that function.
Therefore, the failure was not caused by Nginx, the server firewall, the API key, or the WeChat login itself.
The plugin and the installed DSH package expected different APIs.
9. Why Simply Upgrading DSH Was Not a Safe Fix
It may be tempting to solve the error by upgrading every DSH package to the latest development version.
That approach was not used here because the DSH package APIs were changing between release candidates.
The installed DSH environment was already using:
@deepseek-ai/dsh-agent-presets 0.1.5-rc.2
and that version still did not provide the function required by the old WeChat plugin.
Blindly upgrading DSH could therefore introduce another compatibility problem.
A better diagnostic sequence is:
Plugin error
↓
Identify missing export
↓
Inspect installed package
↓
Check whether export exists
↓
Determine version compatibility
↓
Replace or patch the plugin
This is more reliable than repeatedly changing package versions without identifying the actual API mismatch.
10. Switch the WeChat Integration to dsh-clawbot
The deployment eventually switched from the old WeChat plugin to dsh-clawbot.
The plugin can be added to the web profile with:
dsh plugin --profile web add github:tangwenhao616-netizen/dsh-clawbot
After installation, inspect the profile.
cd /root/.dsh/profiles/web
cat package.json
Then inspect installed packages.
pnpm list --depth 1
The purpose of this check is to confirm that the new plugin is actually present and that the old incompatible WeChat plugin is no longer part of the profile.
11. Start DSH Manually Before Creating a systemd Service
Before relying on systemd, start DSH manually.
dsh web --no-open --trusted-host <SERVER_IP>
The server IP should be replaced with the actual server address.
The deployment keeps the DSH service bound to localhost.
The web service listens on:
127.0.0.1:3080
This is preferable to directly exposing the application port.
If the process starts successfully, test it locally from the server.
curl http://127.0.0.1:3080
The exact response depends on the DSH web application, but the important point is that the TCP connection reaches DSH.
12. Configure Nginx as the Public Entry Point
Because DSH listens on localhost, Nginx is used as the public reverse proxy.
A basic configuration is:
server {
listen 80;
server_name YOUR_DOMAIN;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
The WebSocket headers are important because the DSH web interface can use long-lived connections.
After modifying Nginx:
nginx -t
If the configuration test succeeds:
systemctl reload nginx
13. Fix HTTP 413 Errors in the Reverse Proxy
During client requests, DSH can return:
HTTP 413
When this occurs behind Nginx, the reverse proxy may be rejecting the request before it reaches DSH.
The Nginx configuration can be extended with:
client_max_body_size 100m;
proxy_request_buffering off;
For example:
server {
listen 80;
server_name YOUR_DOMAIN;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
Then test and reload:
nginx -t
systemctl reload nginx
14. A 401 Response Does Not Automatically Mean the API Key Is Wrong
One of the tests against the DSH API returned:
401 Unauthorized
For example, an API endpoint such as:
/api/llm/listProviders
may require a valid browser session.
Therefore, a 401 response from the web API should not immediately be interpreted as a failed DeepSeek API Key.
There are several independent authentication layers in this deployment.
Browser session
│
▼
DSH Web API
│
▼
DeepSeek API credentials
│
▼
DeepSeek model
A browser-session problem and a DeepSeek API-key problem can therefore produce completely different symptoms.
15. Run DSH with systemd
Once manual startup works, systemd can manage the service.
A service definition can use:
[Unit]
Description=DeepSeek Harness Web
After=network.target
[Service]
Type=simple
WorkingDirectory=/root/.dsh
ExecStart=/usr/bin/dsh web --no-open --trusted-host <SERVER_IP>
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
The exact path to dsh should be verified with:
which dsh
After creating or updating the service:
systemctl daemon-reload
systemctl enable dsh
systemctl restart dsh
Check its status with:
systemctl status dsh --no-pager
For detailed startup errors:
journalctl -u dsh -n 50 --no-pager
If the service repeatedly restarts, the journal is usually the first place to inspect.
16. Troubleshoot DSH Startup Failures in the Right Order
When DSH fails to start, checking components in sequence saves considerable time.
Use this order.
Step 1. Check Node.js
node -v
Step 2. Check DSH
dsh --version
Step 3. Check the profile JSON
cat /root/.dsh/profiles/web/package.json
Validate it:
node -e "JSON.parse(require('fs').readFileSync('/root/.dsh/profiles/web/package.json','utf8')); console.log('OK')"
Step 4. Check installed packages
cd /root/.dsh/profiles/web
pnpm list --depth 1
Step 5. Check credentials
cat /root/.dsh/.credentials.yaml
Do not expose secrets while doing this.
Step 6. Start DSH manually
dsh web --no-open --trusted-host <SERVER_IP>
Step 7. Check systemd logs
journalctl -u dsh -n 50 --no-pager
Step 8. Check Nginx
nginx -t
Then:
systemctl status nginx --no-pager
This separates application errors from reverse-proxy errors.
17. The Main Errors and Their Actual Causes
The key is to classify the failure before changing the configuration.
18. Recommended Deployment Structure
After the migration, the intended structure is:
/root/.dsh/
├── .credentials.yaml
└── profiles/
└── web/
└── package.json
The public request path is:
Internet
│
▼
Nginx
│
▼
127.0.0.1:3080
│
▼
DSH Web
│
├── DeepSeek credentials
│
└── dsh-clawbot
This keeps the DSH application itself behind the reverse proxy.
19. Deployment Checklist
Use the following checklist when deploying the same architecture on another Ubuntu server.
-
[ ] Confirm Ubuntu is available. -
[ ] Install Node.js. -
[ ] Verify node -v. -
[ ] Verify npm and pnpm. -
[ ] Install DeepSeek Harness. -
[ ] Verify dsh --version. -
[ ] Create or inspect the webprofile. -
[ ] Validate package.json. -
[ ] Remove incompatible plugins from bundles. -
[ ] Verify .credentials.yaml. -
[ ] Use numeric version: 1. -
[ ] Configure the DeepSeek API key without printing it. -
[ ] Install dsh-clawbotif it is the selected WeChat integration. -
[ ] Run DSH manually. -
[ ] Confirm DSH listens on 127.0.0.1:3080. -
[ ] Configure Nginx. -
[ ] Enable WebSocket proxy headers. -
[ ] Add the request-size settings if HTTP 413 occurs. -
[ ] Run nginx -t. -
[ ] Reload Nginx. -
[ ] Create the systemd service. -
[ ] Check systemctl status dsh. -
[ ] Check journalctl -u dshwhen startup fails. -
[ ] Test the browser session separately from the DeepSeek API credentials.
20. Frequently Asked Questions
Why does DSH say that Node.js cannot be found?
Node.js is not installed or is not available in the current PATH. Install Node.js first and verify it with node -v.
Why does editing package.json cause DSH to fail immediately?
DSH cannot load the profile if package.json contains invalid JSON. Validate the file before starting DSH.
Why must credentials use version: 1?
The installed credentials package expects the document version to be the numeric value 1. A quoted "1" has a different YAML type.
Why did the old WeChat plugin fail?
The installed DSH preset package did not export resolveSessionPreset, while the old WeChat plugin attempted to import that function. The plugin and DSH versions were therefore incompatible.
Should DSH listen directly on the public IP?
The deployment keeps DSH on 127.0.0.1:3080 and uses Nginx as the external entry point.
What does HTTP 413 mean in this setup?
It indicates that the reverse-proxy layer rejected a request because of its configured request handling limits. Increasing client_max_body_size and disabling request buffering can address this case.
Does a 401 response mean the DeepSeek API key is invalid?
Not necessarily. DSH’s web API can require browser-session authentication, so the browser session should be checked separately from the DeepSeek API credentials.
How should a DSH service restart loop be diagnosed?
Start with:
systemctl status dsh --no-pager
Then inspect:
journalctl -u dsh -n 50 --no-pager
The first startup exception usually identifies whether the problem is the profile, credentials, plugin compatibility, or another runtime component.

