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:LISTENWindows (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 3100This 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-configThen 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 -1A 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.