An operating position, not a demo

Every claim on this page is either a config value you can read in this repository or a decision documented, with its reasoning, in CLAUDE.md. Nothing here is aspirational.

1image, 4 roles
2docker networks
0published workspace ports
7podman API groups exposed
2ws frame types, never mixed
24+AI harnesses, one image
2IP families served, v4 + v6
2SSH host key algorithms
2web deploy slots, blue-green

The system, top to bottom

One image, dpe-box:latest, plays four roles by environment variable alone: DPE_ROLE=web runs the browser-facing orchestrator, DPE_ROLE=ssh runs a real SSH server (dpe-sshd), DPE_ROLE=agent-gateway runs dpe-gatewayd, the control plane every workspace's own dpe-agentd connects to, and anything else runs a member's own workspace. None of the three infrastructure roles touch the Docker socket directly; all three reach it through the same locked-down proxy that can list, create, and exec into containers and nothing else. Every workspace container sits on a second, NAT-only network with no published ports at all, ever, on any workspace container, at the Docker level: nothing on the internet reaches one directly. A member can still put one specific port of their own on the public internet, deliberately, through the agent gateway described below; that path never publishes a Docker port either, see that section for exactly how it works instead.

graph TD
    Browser["Browser
xterm.js, installable PWA"] SSHClient["Any real SSH client
ssh / sftp / -L forwarding"] Molohttp["molohttp
TLS termination, 127.0.0.1 only"] Web["dpe-web
FastAPI, DPE_ROLE=web, active slot"] WebB["dpe-web-b
idle blue-green slot"] Ssh["dpe-ssh
asyncssh, DPE_ROLE=ssh, port 222"] Gateway["dpe-gateway
DPE_ROLE=agent-gateway"] Proxy["podman-socket-proxy
7 scoped API groups only"] Sock[("/var/run/docker.sock")] DevPlace["devplace.net
auth-token + profile scrape"] WSA["workspace: member A
dpe-ws-<hash>"] WSB["workspace: member B
dpe-ws-<hash>"] WSn["... one container per member
never deleted, only started/stopped"] Browser -->|"wss:// https://"| Molohttp Molohttp -->|"active slot, cutover on deploy"| Web Molohttp -.->|"idle until the next deploy"| WebB SSHClient -->|"port 222, direct"| Ssh Web -->|"tcp, dpe_control network
internal, no internet egress"| Proxy Ssh -->|"tcp, dpe_control network
internal, no internet egress"| Proxy Gateway -->|"tcp, dpe_control network
internal, no internet egress"| Proxy Proxy -->|"read-only bind mount"| Sock Sock -.manages via podman API.-> WSA Sock -.manages via podman API.-> WSB Sock -.manages via podman API.-> WSn Web -->|"own DevPlace identity only"| DevPlace Ssh -->|"own DevPlace identity only"| DevPlace Ssh -.->|"direct tcp, dpe_workspaces network, local port forwarding only"| WSA Gateway -.->|"one outbound WS per workspace
see the agent-gateway diagram below"| WSA subgraph net_workspaces["dpe_workspaces network: NAT egress out, nothing published in"] WSA WSB WSn end

The agent-gateway's own routing (domain pool, reverse proxy, admin surface) is drawn in full detail in its own diagram further down this page; this one only places it among the other three infrastructure roles.

The terminal wire protocol: no envelope, because the transport already has one

Most home-grown terminal-over-websocket designs wrap every byte in a JSON envelope or base64 it "to be safe." That is unnecessary work that also risks corrupting binary output, and this system does not do it: a WebSocket frame is already typed, binary or text, at the protocol level, so /ws/terminal uses that native distinction as the entire protocol. Binary frames are raw PTY bytes, both directions, no encoding step to get wrong. Text frames are a two message JSON control channel (init, resize). That is the whole protocol. One socket, two channels, multiplexed by the transport's own frame type instead of a hand-rolled header byte.

sequenceDiagram
    participant B as Browser (xterm.js)
    participant W as FastAPI (TerminalBridge)
    participant C as tmux attach (docker exec, user dev)

    B->>W: wss:// connect /ws/terminal
    B->>W: text frame  {"type":"init","cols":120,"rows":32}
    W->>C: docker exec -u dev tmux new-session -A -s main
    loop live session, unbuffered
        C-->>W: raw PTY bytes
        W-->>B: binary frame (PTY bytes, zero envelope)
        B->>W: binary frame (keystrokes / paste)
        W->>C: raw bytes written straight to the PTY
    end
    B->>W: text frame {"type":"resize","cols":.., "rows":..} (80ms debounced)
    W->>C: exec_resize (executor thread, never blocks the event loop)

Every blocking Docker SDK call reachable from this path runs in an executor thread; the byte pump itself uses raw asyncio.loop.sock_recv/sock_sendall on the hijacked exec socket, native non-blocking I/O with no thread pool in the hot path. That split, real async where it matters, threaded where a library forces it, is what keeps one member's slow Docker call from ever stalling another member's keystrokes.

Embedding the terminal elsewhere: one script tag

Section 14's own custom-element rewrite exists specifically so the terminal is a genuine, self-contained component, not a page. Dropping it into another page, on this same origin or a different one, is one script tag:

<script src="https://dpe.cloud.pravda.education/static/js/embed.js"
        data-skin="molodetz"
        data-target="#dpe-mount"></script>

The loader injects the terminal's own stylesheet, xterm and its addons, and the same <dpe-terminal-app> element and app.js module this product's own terminal.html renders, no separate build step and no bundler on the host page's side. Every asset it requests is resolved against its own <script> tag's origin, never the host document's, so it works correctly regardless of which site loads it.

AttributeOnWhat it does
data-skinthe loader's own <script> tagForwarded straight onto the created <dpe-terminal-app data-skin="...">. Any registered skin id works (Section 65): default, molodetz, devplace. Omit it for default.
data-targetthe loader's own <script> tagA CSS selector for an existing element to mount the widget into. Omitted: the widget is inserted immediately after the loader's own script tag, the ordinary widget-loader convention.
data-skin<dpe-terminal-app> directly, if you build the markup by hand instead of using the loaderThe same skin override, read straight off the component itself: CSS custom properties set on any element cascade to that element's own descendants regardless of what the surrounding page declares, so this works identically whether or not the host document has a data-skin of its own on <html>.

What this does not do: authenticate a visitor. The terminal still reaches /ws/terminal with this product's own SameSite=Lax session cookie (Section 6); that cookie is only ever sent on requests to this product's own origin, so an embedded widget on a genuinely different site renders correctly (the shell, the xterm view, the connection indicator) and then shows a closed connection, honestly and immediately, exactly the same handshake rejection a signed-out visitor's browser already gets attempting this same endpoint on this product's own pages, never a silent hang or a broken-looking crash. Embedding on this same origin, where the cookie already applies, works exactly as this product's own terminal page does today, with nothing further needed. A real cross-origin authenticated embed is a separate, security-relevant decision (Section 14 already names exactly what it would take) and is not something this loader attempts on its own.

The clipboard bridge (OSC 52 from vim/tmux, and any text selected in the terminal, landing in the real system clipboard) rides on navigator.clipboard, which only exists in a secure context. Embedded directly into the host page's own DOM (this loader never uses an iframe, so no separate frame-permissions policy is involved), the widget runs under the host page's own origin and security state: on an HTTPS (or localhost) host page the clipboard bridge works exactly as it does on this product's own pages; on a plain-HTTP host page navigator.clipboard is unavailable and the bridge degrades to a no-op, the terminal itself still works normally, copy and paste just fall back to whatever the browser's own default terminal text-selection/paste handling offers.

Isolation: what "sandboxed" means here, specifically

LayerWhat actually enforces it
FilesystemNo volume, no bind mount, for any workspace user data, ever. A container's $HOME lives only in its own writable layer, bounded by a kernel-enforced storage_opt quota.
Capabilitiescap_drop: ALL, then an explicit allow-list matching Docker's own unprivileged default, minus NET_RAW/NET_ADMIN: packet spoofing and sniffing between co-tenant containers on the same bridge network is closed off entirely, on purpose.
sudoReal root inside the container, capped by the same bounding set either way: no SYS_ADMIN, no SYS_MODULE, no SYS_PTRACE, no Docker socket. Root in here still cannot leave here.
NetworkTwo networks: an internal, no-egress one carrying only Docker-API traffic between the orchestrator and the socket proxy, and a NAT-egress one for every workspace container, with zero published ports on any of them.
Docker API surfaceNeither the web, ssh, nor agent-gateway service mounts /var/run/docker.sock. All three talk to one proxy locked to seven scoped API groups; volumes, swarm, system and secrets are hard-disabled at the proxy, not just unused.
CredentialsEvery workspace container gets exactly one member's own DevPlace API key, read from that member's own account, injected only into that member's own container. No operator key, no shared budget, ever crosses a container boundary.
IdentityA member's container name and storage path are both derived with SHA-256 from their DevPlace username; there is exactly one identity scheme, never a second one layered on top, regardless of whether they type that username or their resolved handle at login.
SSH forwardingLocal port forwarding (ssh -L) is validated against the caller's own container before it is ever allowed; remote forwarding (ssh -R) is refused unconditionally by default.

The agent gateway: a public URL for one port, with zero published Docker ports

dpe-agentd runs inside every workspace container, one outbound WebSocket to dpe-gateway's control port, nothing ever listens for it to be reached. That single, member-initiated outbound connection is the entire mechanism; no Docker port is ever published on a workspace container to make this work, the same invariant the isolation table above already holds for everything else.

graph TD
    Svc["a member's own process
listening on one port, inside their container"] Agentd["dpe-agentd
inside that same workspace container"] GwControl["dpe-gateway
control port 9000, internal only"] Pool[("domain pool
pre-issued hostname + certificate")] Molohttp["molohttp
dynamic site API"] Internet["public internet"] GwProxy["dpe-gateway
proxy port 21214"] Admin["operator
current DPE admin (oldest active member)"] GwAdmin["dpe-gateway
admin port 9001"] Agentd -->|"outbound only
HMAC handshake + real container-IP check"| GwControl Agentd -->|"port opened"| GwControl GwControl -->|"assign()"| Pool Pool -->|"issue_certificate / list_sites"| Molohttp Internet -->|"https://<hash>.cloud.pravda.education"| Molohttp Molohttp -->|"reverse proxy"| GwProxy GwProxy -->|"direct HTTP/WS, dpe_workspaces network
never through the control WS"| Svc Admin -->|"authenticated session"| GwAdmin GwAdmin -->|"cmd_request / file_get / file_put
same control WS, fully audited"| GwControl GwControl -.->|pushed to| Agentd subgraph one_container["one member's own dpe-ws-<hash>, zero published Docker ports throughout"] Svc Agentd end
ParameterValue
Handshakeserver nonce + HMAC-SHA256 proof over a per-member agent_token (32 random bytes, generated once, stored on that member's own record, never another member's); the claimed container name's real Docker-network IP is independently cross-checked against the actual peer address before authentication succeeds
Token deliverybase64-framed export lines in .dpe-env (dpe.dpeenv.DpeEnvFile), the same live-pushable, quote-safe mechanism already used for DEVPLACE_API_KEY/DEVPLACE_HANDLE
Exposure scopeone identity_hash can only ever have its own ports assigned a pool hostname; the proxy resolves the target container fresh, by identity_hash, on every single request, never a cached address
Reachabilityan auto-claimed port is public (no auth) immediately by default (Settings.auto_confirm_exposed_ports); dpe-expose still exists on top of that to add HTTP Basic Auth (PBKDF2-hashed, member-set), and a deployment can turn the default off to require an explicit dpe-expose call before anything is reachable at all, in which case an unconfirmed claim stays indistinguishable from an unclaimed one
Hostname releasefreed within about one poll interval (~2 seconds) once the member's own agent reports the port genuinely closed, never the old day-long catch-all grace window, which still exists but only ever fires when nothing is left running to report a graceful close; if the pool has nothing else ready to hand out, assignment also reclaims the oldest inactive hostname, the calling member's own first, a stranger's only if they have none, and never one still genuinely open
Re-notificationa toast fires again whenever a port is still, or becomes again, reachable, not only the first time; bounded by a cooldown so a routine ~10-second keep-alive re-announcement never re-toasts a port that was never actually closed
Admin control surfaceping / exec / file-get / file-put across any connected agent, gated on dpe.adminrule.is_admin() (the oldest still-active registered member, computed live from real data, never a stored flag), every call fully audited (caller, target, command or path, outcome)
Ports published on the gateway itself21214 (proxy, molohttp-facing) and 9001 (admin, loopback) only; 9000 (control) is not published at all, reachable only from containers already on dpe_workspaces

Decisions with numbers behind them, not guesses

ParameterValueWhy this number
Memory / swap2 GiB / 2.5 GiBDoubled from 1 GiB on an explicit request, still with 512 MiB of swap headroom so a brief spike degrades instead of an instant kill. Originally 256 MiB, raised after a real OOM kill on a live harness.
CPU2 vCPURaised from 0.5 to 2 on the same request. Still the anti-abuse control for CPU-bound misuse, just a looser one; memory is not that control.
Disk2 GBKernel-enforced storage_opt quota where the host storage driver supports it; the code detects and reports when it silently cannot.
Process slots4096Raised from 1024 (itself raised from 256 after a real fork-exhaustion) to 4096 on the same request. Enough for real parallel compiles; not enough for a fork bomb to matter.
Resize debounce80 msCoalesces a resize storm (window drag, mobile rotation) into one exec_resize call instead of dozens.
escape-time0 msRemoves the default Escape-key lag every unconfigured tmux has.
Scrollback50,000 linesLong enough for a real working session's history to still be there when you need it.
Upload progress refreshforced, not polledtmux refresh-client -S is issued on every reported step, instead of waiting on the default 5s status-interval, so the status bar reflects reality immediately.
Credential cache TTL24 hoursLong enough that a repeat login skips devplace.net entirely; short enough that a rotated password is never trusted stale for more than a day.
Credential hashPBKDF2-HMAC-SHA256, 200,000 roundsThe one value cached from a login is never the password itself, only a deliberately slow, per-member-salted derivation of it.
Generic cache defaults5,000 entries, 24h TTLEvery cache in this codebase (user records, the handle index) is the same dpe.cache.Cache class, sized the same way, so behavior stays predictable everywhere it is used.
Port re-notify cooldown300 secondsLong enough that a port's own routine ~10-second keep-alive re-announcement never re-toasts a member for a port that was never actually closed; short enough that nobody has to remember an hours-old toast to know a port is still live.

Live cache stats, from this process, right now

Every cache in this codebase is one dpe.cache.Cache instance (in-process, LRU-bounded, optionally TTL'd); this table is not a mockup, it is Cache.registry() read at the moment this page was rendered.

CacheEntriesMax sizeTTLHitsMissesHit rate
user-records 1 5000 no expiry 84 1 99%
current-admin 0 1 30s 0 0 n/a
handle-index 0 1 no expiry 0 0 n/a
internaltrust.container_peers 0 32 15.0s 0 0 n/a
internaltrust.network_gateways 0 32 15.0s 0 0 n/a
exposed-ports 0 5000 no expiry 0 0 n/a
domain-pool 0 1 no expiry 0 0 n/a
ratelimit.login 1 20000 120s 1 1 50%
ratelimit.upload 0 20000 120s 0 0 n/a

Rate limiting: security and shared-host courtesy in one mechanism

Both the login form and the file-upload endpoint sit behind a small in-process sliding-window limiter (dpe.ratelimit.RateLimiter), keyed on the real client address as resolved by uvicorn's own trusted-proxy handling (request.client.host, only ever rewritten from X-Forwarded-For when the immediate peer is molohttp itself, per Settings.trusted_proxy_ips; a caller cannot spoof its way past this by supplying its own forwarded-for header). Sign-in attempts are capped at 10 per minute per address: enough for a real person who mistyped a password, nowhere near enough for credential stuffing against DevPlace's own auth endpoint on this box's behalf. Uploads are capped at 30 per minute per address: this exists for resource courtesy as much as security, since every reported upload step means a real docker exec call into a container, and a shared host has no interest in one member's script turning that into a flood. A limit hit returns a plain 429 with a Retry-After header, not a silent drop. The SSH gateway runs the same idea twice over, independently: 50 authentication attempts per 60 minutes per source address, and separately per attempted username, so neither "many usernames from one address" nor "one username from many addresses" can turn this box into a hammer against DevPlace's own login endpoint; a short, explicit allowlist of this host's own test/benchmark addresses (Settings.ssh_rate_limit_trusted_nets) bypasses this specific limiter, never a real member's traffic. A fourth, independent instance guards the agent-gateway's own handshake: 60 attempts per 30 seconds per source, sized specifically so an entire fleet of dpe-agentd processes reconnecting at once after a dpe-gateway restart self-heals in well under a minute rather than locking itself out for the better part of an hour; each reconnect also waits a small random jitter (Settings.agent_reconnect_jitter_seconds) so a fleet-wide restart does not retry in perfect lockstep.

Deploys: blue-green for web, live-provisioning for everything else

Two separate deploy paths, for two structurally different targets. A workspace container is never recreated for an ordinary code change: scripts/live-provision.sh docker cp's an updated binary or config file straight into every already-running container's filesystem, no restart. web is different, a single shared process every member's browser tab routes through, so a naive recreate drops every open /ws/terminal connection at once. Fixed with a real blue-green swap, not a warning label.

ParameterValue
Slots2, dpe-web (port 21212) and dpe-web-b (port 21213), one docker-compose.yml YAML anchor, one difference: container name and published port
Cutover mechanismedit /etc/molohttp/config.json's one upstream URL for site_dpe_cloud_pravda, SIGHUP reload, never restart
Upstream resolution pointonce, at each WebSocket's own upgrade handshake (molohttp's own WSTunnel.swift); never re-consulted for that connection's remaining lifetime
Reload side effect on an open connectionnone; ConfigManager.swift's reload() only replaces molohttp's in-memory config, touches no listener, no socket
Drain signalGET /internal/drain-status, trusted only from real loopback plus Settings.drain_status_trusted_ips (a host-to-published-port call hairpins through the dpe_workspaces bridge gateway address, never literal 127.0.0.1), backed by dpe.connectiontracker, a lock-protected in-process counter
Idle-slot retirementnone, on a timer; the previous slot is recreated on the next deploy, after its own drain check
First-ever deploy through this pathdrains against a pre-drain-status container; falls back to one fixed grace wait instead of polling a route that does not exist yet
SSH equivalentnot built; molohttp has no TCP-passthrough handler type at all, only HTTP/WS. Design: a 5th DPE_ROLE, a small dependency-free asyncio TCP splice, same two-slot shape. Deferred because a dropped SSH connection costs one ssh re-run, not a dead browser tab.

Config-push path, for shared, non-secret environment values: every workspace container's /root/.bashrc and /home/dev/.bashrc source /etc/dpe-env-loader.sh, which sources, with auto-export (set -a), any of /dpe/.env, /dpe/.env.*, /dpe/*.env. /dpe is already a live, read-only bind mount (see the isolation table above), so a file dropped there on the host reaches every already-running container's next shell with zero rebuild, zero recreate, zero deploy step of any kind. Never a channel for a per-member secret, which stays on the separate, already-existing .dpe-env/DEVPLACE_API_KEY path.

Two real paths exist for shipping a code change, and the Makefile itself is explicit about which one a live deployment needs. make provision (image build, a plain docker compose up -d web, then live-provision.sh's filesystem-only patches to every already-running workspace container) is for first-time bring-up and local development; its own naive web recreate drops every open terminal connection with no drain, no health check, and no rollback, and the target prints that warning every time it runs. make deploy-web (scripts/deploy-web.sh) is the blue-green path above, the one an already-live deployment with real member sessions actually needs. It is also a real sudo edit to molohttp's own shared config, the same reverse proxy fronting every other site on this host, not just this one; that step is run by a human, at a real terminal, every time, including by an AI session working on this codebase, which builds, tests, and verifies the deploy script but never executes it unattended. The two commands below are held to the identical standard, for a sharper reason: they destroy data, not just risk a dropped connection or a shared config edit.

Destructive commands require a human who typed the words, every time

Two commands in this codebase can destroy real data, and both are built the same way: print a full, real report of exactly what is about to go, then refuse to proceed without a human typing an exact phrase at a real, connected terminal.

CommandDestroysConfirmation phraseFails closed when
make prune-workspacesEvery workspace container (never data/users/*.json, member records survive it)delete all workspace dataNo real, connected terminal (/dev/tty); a piped or pre-written answer hangs, it does not proceed
make purgeEvery workspace container and every stored member recordpurge everything permanentlySame /dev/tty gate; the operator sees every affected username first, never an API key

An AI session working on this codebase may build these commands, verify their gate correctly blocks a wrong or missing answer, and show the report they print. It never supplies the actual confirmation phrase itself, under any framing, including one that seems, in the moment, like an obviously wanted outcome. If a human wants either command to actually run, they type the phrase themselves.

How a member's data survives us

A workspace container is created exactly once per member and afterward only ever started or stopped, never deleted, which is what makes persistence work with no volume at all (see the isolation table above): the container's own writable layer, bounded by its storage_opt quota, is the persistence layer. Nothing routine in this system, an image rebuild, a web deploy, a live-provisioning pass, ever touches that container's filesystem in a way that could lose what is on it.

What could lose dataWhat actually stops it
A maintenance pass judging a container "probably empty" from a quick lookNot a judgment call: scripts/workspace-data-report.sh runs docker diff against every container in scope and prints a real verdict; a human sees that report, container by container, before anything destructive proceeds
A general "yes, go ahead" from earlier in a conversation being read as blanket permissionDoes not count; the confirmation above has to be given after the report is already in front of the human, for that specific, named action
A member record's stored secrets (devplace_api_key, agent_token, cached password hash) leaking through a log line or a response bodyNone of them are ever logged or returned in any response; the cached password value is never the password itself, only a per-member-salted PBKDF2 derivation of it (200,000 rounds), and only ever compared, never decrypted back out
A JSON record file left half-written by a crash mid-saveEvery write goes to a temp file first, fsync'd, then atomically renamed into place (os.replace); a reader never observes a partial file
A member's admin/API-provider spend being drained by someone else's trafficEvery credential injected into a workspace container is that one member's own, sourced from their own authenticated login; no operator key and no other member's key is ever forwarded into any container, under any circumstance

The one honest limit, stated plainly rather than glossed over: this is quota and process-lifetime protection, not off-host backup. A host failure below the container layer (disk hardware, the host itself) is not covered by anything on this page, and nothing here claims otherwise.

Security rules, in one place

Every rule below is enforced by real, running code, most of it detailed elsewhere on this page; this table exists so the whole security posture can be read in one pass instead of assembled from the rest of the page by hand.

RuleEnforced by
A workspace never gets more Linux capabilities than it needs, and never the ones that would let it touch another tenant's trafficcap_drop: ALL plus an explicit allow-list, NET_RAW/NET_ADMIN withheld even though sudo itself is granted; see the isolation table above
No process outside a container ever gets the real Docker socketA single socket-proxy, scoped to seven read/attach API groups; volumes, swarm, system, and secrets are hard-disabled at the proxy itself
A member's traffic never reaches another member's container, by network position aloneZero published ports on any workspace container, ever; the agent-gateway proxy resolves a target by identity_hash fresh on every request, never a cached address
An auto-detected open port's reachability is a deliberate, explicit setting, never an accidentThe confirmed gate: by default (Settings.auto_confirm_exposed_ports) a claim is confirmed, and therefore reachable, the instant a hostname is assigned; a deployment can flip that setting off to require the member to run dpe-expose themselves first, in which case an unconfirmed claim proxies identically to an unclaimed one
Login and upload endpoints cannot be hammered through this box against DevPlace's own serversSliding-window rate limits, keyed on the real client address, never a spoofable header; see the rate-limiting section above
A forwarded client address or protocol is only ever trusted from this host's own known proxy hopuvicorn's own forwarded_allow_ips, scoped to Settings.trusted_proxy_ips; nothing reads X-Forwarded-For by hand
A session cookie cannot be replayed across sitesSameSite=Lax on every session cookie this product issues, Secure whenever the request is genuinely HTTPS
Publicly cacheable assets can be loaded cross-origin; anything that touches a session cookie cannotAccess-Control-Allow-Origin: * is set on exactly one mount, /static/* (needed for the embed loader above); no authenticated route sets any CORS header at all
Admin authority on the agent-gateway is never a flag someone could leave mis-setComputed from real data (the oldest still-registered member whose workspace container still exists), never a stored boolean; a 30-second cache only bounds how often a burst of calls re-scans, and is invalidated immediately on the same SIGUSR1 every other in-process cache already listens for
A shell command built from untrusted text (a filename, a clipboard paste) cannot become a shell injectionBase64 the value, shlex.quote the base64, decode it back inside the container; the one sanctioned pattern everywhere a value crosses that boundary
Destroying data always needs a human who has already seen what it costsThe confirmation-phrase gate above, plus the per-container report-and-ask procedure this product's own house rules hold every maintenance action to

Verified against the real, running stack, not against a mental model of it

Every one of the following was a genuine, shipped defect, each caught by actually connecting to the real service and only fixed once reproduced, never guessed at from reading the code. tests/ exists so the next one is caught the same way, automatically, instead of by a member reporting it: tests/integration/ drives the real dpe-web/dpe-ssh containers and a real, disposable DevPlace.NET account over genuine SSH/HTTP, never a mock of Docker or of DevPlace; tests/e2e/ drives a real Chromium browser against the real login and terminal pages. A separate, scoped review-agent system under .claude/agents/ (security, SSH-gateway correctness, container isolation, caching, performance, code quality, agent-gateway/event-protocol correctness, data-safety/never-lose-data, and public-docs accuracy) checks future changes against exactly these bug classes before they ship again; none of it is allowed to run a destructive command against a member's data on its own.

FoundWhat broke, and how it was caught
Startup raceA brand-new container's own tmux bootstrap could lose a race against the very first incoming exec, throw duplicate session: main under set -eu, and kill the container's PID 1 mid-login. Fixed with an idempotent has-session || new-session in entrypoint.sh and tmux's own atomic new-session -A on every attach path.
Non-interactive exec hangs forevereof_received returning False made asyncssh close the reply channel's own send side the instant a non-interactive client sent stdin EOF, before any output had been written. Fixed by returning True and shutting the Docker exec socket's write half explicitly once the pump loop ends.
IPv6 clients silently blackholedPort 222 was published as 0.0.0.0:222:222, IPv4 only. An IPv6-preferring client's SYN hit this host's default-deny firewall with no RST at all, indistinguishable from a hang until a long OS timeout gave up. Fixed by dropping the explicit host IP so Docker's own docker-proxy binds both families, exactly like every other port-published service on this host already does.
Piped stdin never terminated the remote commandA non-tty exec's own input pump stopped forwarding bytes on the SSH channel's EOF but never closed the write half of the underlying Docker socket, so any remote process reading its own stdin to completion (wc -c, cat) blocked forever waiting for more. Fixed with an explicit socket.SHUT_WR once the input pump loop ends, non-tty sessions only.
SSH exit codes were always 0Every non-interactive ssh host cmd reported success regardless of the real outcome. Fixed by querying the real exec result via Docker's own exec_inspect and passing that exit code back to the channel.
One host key type onlyA real, narrowly-built client (ChromeOS Secure Shell) could not complete key exchange against an Ed25519-only server and failed with an opaque, non-protocol error. Fixed by generating and offering an RSA host key alongside Ed25519, never instead of it.
SFTP binary missing on old containersAn already-running container created before openssh-sftp-server was added to the image never picked it up, because the live-provisioning retrofit script hadn't been updated to match. Fixed by adding the package to both the image and the retrofit list, and confirmed the fix live, without recreating the container.
Concurrent first-ever logins could reject each otherTwo connections racing get_or_create_container for the same brand-new identity meant the loser of Docker's own container-name uniqueness check got an unhandled 409 Conflict, rejecting that login outright. Fixed by catching exactly that status and retrying a fetch of the container the winner just created.
A hidden overlay that was never actually hiddenThe upload dropzone's own class declared display: flex, which beats the browser's default [hidden] rule regardless of the hidden attribute's state, so it sat visible over the terminal permanently. Fixed with an explicit, higher-specificity [hidden] companion rule.
Deleting a user record on disk did nothingweb and ssh each cache user records in-process; deleting the JSON file underneath a warm cache brought the "deleted" record right back on the next login. Fixed with a SIGUSR1 handler in both running processes that clears the real, live cache, not a throwaway interpreter's copy of it.
That same SIGUSR1 fix could never actually arriveentrypoint.sh execed the app under su dev -c '...', which manages its own session and does not forward signals to its child; a real docker kill -s SIGUSR1 reached su and stopped there. Fixed by replacing su with setpriv (a direct, no-fork privilege drop), collapsing the process tree to exactly tini → dpe / tini → dpe-sshd, plus the one added KILL capability web/ssh needed for a cross-uid signal to be forwarded at all.
An event's own fields could silently overwrite its envelope typeThe agent-gateway wire protocol builds each frame as {"type": ..., **fields}; a field literally named type (which every entry in the real event catalog carries) clobbered the envelope's real frame type on the wire, corrupting anything that shape reached. Fixed by nesting fields under its own key instead of flattening it into the envelope dict, with a unit test reproducing the exact collision; a second, independent instance of the same landmine was later found and fixed in the browser-notification path that flattened the same way.
A successful browser notification looked exactly like a hangdeliver_to_browser's failure branch correctly told the caller "no browser tab was open," but its success branch simply returned, telling the caller nothing; every fire-and-forget notify/render-HTML call waited its full timeout even when delivery genuinely succeeded. Widening the timeout looked like partial progress and was not the fix. Fixed by sending an explicit, immediate result frame on the success path too.
An auto-detected open port was reachable from the public internet before anyone asked for thatThe port-exposure feature claimed a real, certified hostname and proxied traffic to it the instant a wildcard-bound port was detected, with no member confirmation and no visible signal, in effect contradicting this page's own no-network-access-without-asking invariant. Fixed by adding an explicit confirmed gate and a proxy-level check that treats an unconfirmed claim exactly like an unclaimed one, plus a toast the instant a port opens. That gate's own default was later reversed, deliberately, back to reachable-immediately (see the Reachability row above); the gate itself is unchanged and is exactly what makes that default safe rather than a repeat of the same defect.
The domain pool grew without boundTopping the pool up minted a brand-new candidate hostname on every check that found nothing immediately retryable, without telling apart "nothing in flight" from "something in flight but still inside its own backoff window"; left running for a couple of days this produced thousands of abandoned, permanently-failed records instead of one hostname retried patiently. Fixed by waiting on any pending issuance before minting a second one, plus a bounded retry cap so a candidate that is genuinely, permanently bad is eventually abandoned rather than retried forever.
A maintenance write could leave the service unable to read its own data backAn ad hoc, no-flag docker exec against a live container wrote a data file as root instead of the real, uid-1000 service user; the file's contents were correct, but the running process that owned it could no longer even open it. Fixed generically, not just for that one incident: the one shared JSON write path now chowns every file it writes back to the real service user immediately before the atomic rename, best-effort, so a mistake like this self-heals on the very next write instead of causing an outage.
A closed port never actually freed its hostname in less than a dayThe workspace agent already sent a real, correctly-shaped "this port just closed" frame within about two seconds of it happening; the gateway's own control-frame dispatcher simply had no branch for it and silently dropped it, every time, since the feature first shipped, leaving only the much slower catch-all grace window to ever reclaim it. Fixed by wiring the existing frame up to release the hostname immediately; the slow window stays as the correct fallback for a container that disappears with nothing left running to report the close gracefully.
A hostname changed outside dpe-expose never reached the memberAn operator-side data fix that rotated which hostname a port mapped to updated the gateway's own records correctly but told nobody; re-running dpe-expose looked like the obvious repair and printed the stale hostname right back, since neither the member's own status file nor the config watcher had any reason to believe anything had changed. Fixed with a real, admin-gated resync path that reads the gateway's own current record and pushes it to the member's agent and browser directly, independent of dpe-expose ever being invoked.
A live link's own trailing punctuation ended up inside the linkThe toast's auto-linkify pattern excluded only whitespace and a literal ) from a URL match, correctly handling one existing, parenthesized message but not a plain sentence's own trailing full stop; the period was swallowed straight into the href, producing a hostname molohttp had never issued a certificate for. Fixed by stripping the same small set of trailing punctuation every mainstream auto-linkifier already strips, then re-inserting it as ordinary text right after the link.

Real numbers, from a real run, not a live gauge

The figures below are a point-in-time snapshot, not a continuously-updated measurement: they came from actually running tests/integration/ssh/test_ssh_benchmark.py against this host's own dpe-ssh container over loopback, once, at the time this section was last edited. They will drift from the real, current numbers as the deployment changes; run make test-integration yourself for what this host does right now, rather than trusting a static table to stay current on its own.

PathMeasuredWhat it means
Bare exec round trip (true, 20 calls)~48 ms averageThe floor cost of one docker exec hop for a non-interactive SSH command; this is a local, same-host figure, not a wide-area one.
SFTP upload, 32 MB~168 MB/sClient to container, through the real sftp-server binary, not a virtual filesystem.
SFTP download, 32 MB~170 MB/sSame path, reverse direction.
Exec-channel upload, 32 MB piped to wc -c~154 MB/sRaw stdin over a non-tty exec channel, demuxed correctly on the way in.
Exec-channel download, 32 MB from /dev/zero~132 MB/sRaw stdout over a non-tty exec channel, demuxed correctly on the way out.

These are same-host, loopback measurements, the honest number for what this deployment's own Docker-exec plumbing costs; they are not a substitute for a real wide-area figure between two separate hosts, which is a different, larger number dominated by network RTT rather than this plumbing. Where a bottleneck has not actually been measured, this page says so rather than inventing a plausible one.

Larger transfers and concurrency, a separate one-time run

A second, independent benchmark, scripts/sshtest.py, against real 1 GB-plus payloads and multiple concurrent streams, over this same host's real network path (molodetz.nl → pravda.education), not loopback. A point-in-time snapshot, like the table above, not a live gauge; re-run the script for this host's current numbers.

Download (server → client)Speed
512 MB~170 MB/s
1 GB~160 MB/s
2 GB~145 MB/s
Upload (client → server, rsync)Speed
64 MB~120 MB/s
128 MB~188 MB/s
256 MB~275 MB/s
Concurrent streams (256 MB total)Aggregate speed
2×~127 MB/s
4×~115 MB/s
8×~85 MB/s
16×~56 MB/s

Concurrency degrades close to linearly past 2 streams: this path is one TCP connection per SSH session, so parallel transfers compete for the same connection's own throughput rather than adding new capacity. Latency over this real network path: ~100–300 ms average and P50, ~130 ms P95. Negotiated cipher: chacha20-poly1305, AsyncSSH 2.24.0.

This page is generated from the same source this whole product is built from; if something here stops matching reality, that is a defect, not a rounding error. See what's actually installed →