DeepSeek Harness Reverse Proxy: public-url & Path Prefix

Author
DeepSeekAgent.io Editorial Team
Published
Updated

DeepSeek Harness Web serves plain HTTP on a loopback address by default. That is appropriate for local use, but it cannot tell a remote browser which HTTPS hostname and path users actually visit. v0.2.1-alpha.1 adds --public-url to advertise that external address in reverse-proxy, tunnel, and path-prefix deployments.

The essential distinction is between listener address, advertised address, and trusted authority. They control reachability, URL generation, and API admission respectively; none replaces the others.

Minimal launch

Assume users visit https://app.example/ui/ while DSH remains on the server's loopback interface:

dsh --profile web \
  --public-url https://app.example/ui/ \
  --trusted-host app.example \
  --no-open

DSH normalizes the advertised root with a trailing slash and uses it for the printed startup URL, DSH_WEB_URL, and Web-surface orientation. The listener continues to serve origin-root routes and never learns about /ui/.

Keep the three addresses separate

LayerExamplePurpose
DSH listenerhttp://127.0.0.1:3080Actual Host listener, reachable only by the local proxy
Public URLhttps://app.example/ui/External entry shown to browsers, UI, and the model
Trusted Hostapp.exampleAuthority admitted by the browser API trust fence

--public-url advertises an address but does not grant trust. Without the matching --trusted-host, the document may load while API calls return 403. --trusted-host is not an account system either. The launch token and subsequent signed cookie authenticate the browser session.

Five jobs the reverse proxy must perform

1. Preserve the browser-facing Host

DSH compares the received Host with accepted authorities. Do not rewrite it to 127.0.0.1:3080; forward the Host the browser used.

2. Strip the mount prefix

A browser request to /ui/api/... must reach the listener as /api/.... Details such as the trailing slash on proxy_pass determine whether the prefix is stripped correctly.

3. Forward WebSocket upgrades

Forwarding only normal HTTP produces a page that opens but cannot sustain a live Session. The Upgrade and Connection headers must reach the listener.

4. Rewrite cookie Path and Secure

The backend issues a host-only Path=/ cookie without Secure. A prefixed deployment must rewrite Path to /ui/; an HTTPS external leg must add Secure.

5. Redirect /ui to /ui/

The trailing-slash /ui/ route is the canonical entry that exchanges a launch token for a Session cookie. Because the backend never sees the stripped external prefix, it cannot repair the wrong external route for the proxy.

Nginx configuration skeleton

This example shows only DSH-specific behavior. Add certificates, access control, and rate limits through the site's established configuration:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name app.example;

    location = /ui {
        return 301 /ui/;
    }

    location /ui/ {
        proxy_pass http://127.0.0.1:3080/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_cookie_path / /ui/;
        proxy_cookie_flags ~.* secure;
    }
}

Do not validate only the landing document. Test launch-token exchange, Session creation, prompt submission, streamed tool output, refresh recovery, and WebSocket reconnection.

Why not listen on 0.0.0.0?

The official CLI intentionally rejects --host 0.0.0.0. The deployment model keeps the loopback listener inside a trusted boundary while a reverse proxy owns TLS, external hostnames, path prefixes, and access policy.

Neither --public-url nor --trusted-host protects the listener port. Publishing port 3080 through a container or firewall rule can bypass proxy-layer TLS, authentication, and cookie rewriting. Expose it only to localhost or a controlled proxy network.

Cloudflare Tunnel and other tunnels

Temporary tunnels often receive a new hostname on every launch. An authority without a port in --trusted-host matches varying ports, but a changed hostname still requires an updated Public URL and trusted host.

For Cloudflare Tunnel, verify that:

  • the Origin Service points to http://127.0.0.1:3080;
  • the browser Host is preserved;
  • WebSocket support is enabled;
  • the external route uses HTTPS;
  • Cloudflare Access or another login layer does not break launch-token navigation;
  • the launch URL token never enters public logs, screenshots, or analytics.

Troubleshooting

SymptomCheck first
Document loads but every API call is 403Does --trusted-host match the browser Host?
/ui/ opens but assets or API calls return 404Is the proxy stripping the prefix correctly?
Interface remains in reconnecting stateWebSocket Upgrade and Connection headers
Refresh requires the launch token againCookie Path and Secure rewriting
/ui and /ui/ behave differentlyBare-mount redirect
The model returns localhost linksIs --public-url advertised to the Web surface?

Security checklist

  1. Keep the listener on loopback and off the public network.
  2. Enforce HTTPS, access control, and rate limits at the external entry.
  3. Set --public-url to the canonical user-visible root, including its path prefix.
  4. List only required authorities with --trusted-host.
  5. Share the launch token only with intended users and keep it out of Referer logs and screenshots.
  6. Scope the cookie to the mount prefix and add Secure on HTTPS.
  7. Test prompts, tool streaming, refresh recovery, and WebSockets end to end.

Related reading

References