dshkit

Fix: port 3080 already in use when running dsh web

The DeepSeek Harness web UI binds 127.0.0.1:3080. How to find what is holding the port, pass --port correctly (argument order matters), and make a different port stick.

Updated 2026-08-132 min
Short answer

Pass a different port after the launcher flags: dsh --profile web --port 3100. Order matters because --port belongs to the app, not the launcher, and the launcher stops parsing at the first token it does not recognise. To find the process holding 3080, use lsof -nP -iTCP:3080 -sTCP:LISTEN.

dsh web serves the browser UI at http://127.0.0.1:3080. Port 3080 is common enough — dev servers, container port mappings, other local tooling — that collisions are routine.

Find what has the port

macOS and Linux:

lsof -nP -iTCP:3080 -sTCP:LISTEN

Windows (PowerShell):

Get-NetTCPConnection -LocalPort 3080 -State Listen | Select-Object OwningProcess
Get-Process -Id <pid>

If the answer is a stale node process from an earlier dsh run that did not exit cleanly, kill it and try again. If it is something you actually need running, move the harness instead.

Move the harness to another port

dsh --profile web --port 3100

This is also why dsh web --port 3100 and dsh --profile web --port 3100 are not always interchangeable in edge cases. When something behaves oddly, use the explicit --profile form.

Make it stick

Passing --port on every launch gets old. Because the port is app configuration, you can set it in a patch layer instead. Find the row that owns it:

dsh --profile web --dump-config

Then target that row from $DSH_HOME/profiles/web/cordis.patch.yml. Remember the resolution order — bundles, then the profile patch file, then the home-level $DSH_HOME/cordis.patch.yml, then --patch. A home-level setting will override your profile-level one, which is worth checking if the change appears to do nothing.

For a one-off experiment, skip the files entirely:

dsh --profile web --patch ./tmp/alt-port.yml

"In use" but nothing is listening

Three usual suspects.

A container publishing the port. docker ps and look at the PORTS column. A published port holds the host binding even when the process inside is idle.

A half-dead process. A previous run that lost its terminal but kept the socket. lsof will still show it; kill by PID.

Interface mismatch. The harness binds 127.0.0.1, not 0.0.0.0. A process bound to 0.0.0.0:3080 conflicts; one bound to a specific external interface may not. If lsof -iTCP:3080 looks empty, re-run it without -sTCP:LISTEN to catch sockets in other states such as TIME_WAIT.

Sanity check after moving

Confirm the new port is actually serving before assuming the change took:

curl -sSI http://127.0.0.1:3100 | head -1

A response line means the harness is up. Connection refused means the flag did not land — re-check argument order first, since that is the cause far more often than the config.

Frequently asked

Why does dsh --port 3100 web fail but dsh --profile web --port 3100 work?

The launcher parses its own flags first and hands everything from the first unrecognised token onward to the app. Put launcher flags such as --profile ahead of app flags such as --port.

Can I expose the UI on my local network?

The default bind is loopback, which is deliberate for a tool that runs shell commands in your repositories. If you genuinely need remote access, put it behind an authenticated tunnel rather than binding a wider interface.

It says the port is in use but nothing is listening.

Usually a previous dsh process that did not exit cleanly, or a container publishing the port. Check both, and on macOS check for a process bound to 127.0.0.1 specifically rather than 0.0.0.0.

Keep reading