Run DeepSeek Harness as an AppVM, step by step
DeepSeek Harness is a brand-new open source AI agent, moving fast and still in developer preview. Give it a machine of its own instead of your laptop, then reach it from any browser, including a phone or a tablet.
Update, 2026-08-23. Now covers
dsh0.1.1-rc.2, adds a prebuilt image so you can skip the build entirely, and trims this walkthrough down to the steps.
DeepSeek Harness, dsh, is DeepSeek’s open source AI agent. Every release so far
is a release candidate, and its own README warns of compatibility-breaking
changes — a good argument for giving it a machine of its own rather than the
laptop with everything else on it.
This runs it as a Virtainer AppVM: a real machine with its own kernel, its own disk, and its own address, reachable from any browser on your network. There are two ways to get the image, and then one way to run it.
What you get
browser ──► <vm-ip>:3080 nginx ──► 127.0.0.1:3081 dsh web
dsh binds loopback and refuses to bind anything else, so nginx sits in front of
it and re-presents every request as loopback. It rewrites Host and Origin,
injects a crypto.randomUUID polyfill that a plain HTTP address does not
provide, and unlocks the Settings panel, which the client disables unless the
browser’s own address is loopback. All three are properties of dsh, not of
AppVM.
This relaxes checks
dshputs up on purpose, for anyone who can reach the VM. It suits a home or lab network. Put a real gate in front of it anywhere else.
Option 1: Pull the prebuilt image
The Virtainer team publishes this image, built from exactly the Dockerfile in
option 2 and refreshed as dsh releases land. The version in the tag is the
dsh version inside it.
quay.io/virtainer/dsh-lan:0.1.1-rc.2
In App images, point the source at that reference rather than pasting a Dockerfile, then skip to Create the AppVM.
Option 2: Build it yourself
Worth it when you want to change something in the file, pin a different dsh, or
watch every layer come from your own host.
Open App images and click Build. Name the template deepseek-harness,
leave the source on Dockerfile, and paste the file below. The build context
stays empty, which is why the configuration arrives through heredocs rather than
COPY.

FROM docker.io/library/node:24-trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates curl iproute2 procps nginx \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g @deepseek-ai/dsh@0.1.1-rc.2 \
&& npm cache clean --force \
&& dsh --version \
&& node -e "require('$(npm root -g)/@deepseek-ai/dsh/node_modules/node-pty')"
RUN rm -f /etc/nginx/sites-enabled/default
COPY <<'CONF' /etc/nginx/conf.d/dsh.conf
map $http_upgrade $dsh_connection {
default upgrade;
'' close;
}
server {
listen 3080;
server_name _;
access_log off;
error_log /dev/stderr warn;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:3081;
proxy_set_header Host 127.0.0.1:3081;
proxy_set_header Origin http://127.0.0.1:3081;
proxy_set_header Sec-Fetch-Site "";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Accept-Encoding "";
sub_filter_types text/javascript application/javascript;
sub_filter_once on;
sub_filter '<head>' '<head><script>(function(){var c=window.crypto;if(!c||typeof c.randomUUID==="function")return;function h(n){return n.toString(16).padStart(2,"0")}function u(){var b=c.getRandomValues(new Uint8Array(16));b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;var s="";for(var i=0;i<16;i++){s+=h(b[i]);if(i===3||i===5||i===7||i===9)s+="-"}return s}try{c.randomUUID=u}catch(e){}if(typeof c.randomUUID!=="function"){try{Object.defineProperty(Crypto.prototype,"randomUUID",{value:u,configurable:true,writable:true})}catch(e){}}})();</script>';
sub_filter 'isLoopbackHostname(pageLocation.hostname)' 'true';
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $dsh_connection;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
CONF
COPY <<'SH' /usr/local/bin/dsh-with-proxy
#!/bin/sh
set -eu
DSH_PORT="${DSH_PORT:-3081}"
RESTART_DELAY="${RESTART_DELAY:-5}"
proxy() {
trap 'exit 0' TERM INT
while :; do
rc=0
nginx -g 'daemon off;' || rc=$?
echo "[proxy] nginx exited ($rc); restarting in ${RESTART_DELAY}s" >&2
sleep "$RESTART_DELAY" || exit 0
done
}
proxy &
exec dsh web --port "$DSH_PORT" "$@"
SH
RUN chmod 0755 /usr/local/bin/dsh-with-proxy \
&& nginx -t
ENV DSH_HOME=/root/.dsh
RUN mkdir -p "$DSH_HOME" /workspace
WORKDIR /workspace
EXPOSE 3080
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
CMD curl -fsS http://127.0.0.1:3080/ >/dev/null || exit 1
CMD ["/usr/local/bin/dsh-with-proxy"]Click Build. Those RUN steps execute on your host, using its network and
its privileges. The image reaches Ready at about 600 MiB.
Pinning 0.1.1-rc.2 is deliberate. When a project warns you it will break
compatibility, a version in the file is how today’s working setup still works
tomorrow.
The reasoning behind the rest of the file is in Writing a Dockerfile for an AppVM.
Create the AppVM
Go to Instances, click New AppVM, and pick the image. Four vCPUs, 4096 MB and 16 GiB of disk suit this workload; the form opens on smaller defaults.

Two settings are worth setting deliberately. Leave Keepalive shell off — it
is for shell-only images, and this is a service image. Set Restart policy to
on-failure, which is what connects the HEALTHCHECK to real recovery.
Storage and Network stay as they are: no extra volumes, DHCP on your VM network. Click Create AppVM.
The instance reaches healthy in under a minute and the list shows the address it took from DHCP. That address belongs to the VM, not to the host, so open it on port 3080 from whatever is nearest — a laptop, a tablet, a phone. The agent is not running on any of them.
Check it works
Open the instance’s terminal and you land in a root shell on the guest:
# ss -tlnp | grep -E '3080|3081'
LISTEN 0.0.0.0:3080 users:(("nginx",pid=144))
LISTEN 127.0.0.1:3081 users:(("node",pid=141))
# curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/
200
nginx holds the network socket, dsh sits on loopback behind it, and a request
through the whole chain returns 200.
Now open the URL in a browser, set a model API key under Settings, choose a workspace, and start a session.