Harbor Desk is an open-source, client-first desktop app for operating remote
Docker Engines. The user configures one Gateway or Docker Engine target and the
desktop detects which it is. A Harbor Desk Gateway is used directly; a raw
Docker Engine gets a short-lived Local Gateway wrapper on a dynamically
assigned 127.0.0.1 port. Every operation still stays behind a Gateway, so the
renderer does not require Docker Desktop, Docker CLI, or a local Docker socket.
Project status: Harbor Desk is an independently implemented, working vertical slice for remote container operations. Its Server Gateway and Local Gateway wrapper are explicit connection modes, not a turnkey production control plane. The production dependencies and hardening still called out below are deliberate release boundaries, not hidden fallbacks.
Harbor Desk uses a familiar container-management workflow, but it does not include Docker Desktop source code, assets, or direct renderer-to-Engine integration.
- Current implementation
- npm server setup
- Supported platforms
- Architecture and trust boundary
- Getting started
- Server-local Engine overlay
- Configuration and deployment safety
- Project layout
- Contributing and security
- License
The repository currently contains a working first vertical slice:
-
Electron + React + TypeScript + MUI desktop shell.
-
Automatic detection of a Harbor Desk Gateway versus a Docker Engine target.
-
Direct Server Gateway connections, or a Local Gateway wrapper that binds only to
127.0.0.1on an OS-assigned port for raw Engine targets. -
A random per-launch desktop session token that protects Local Gateway API calls, including requests from the packaged renderer's
file://origin. -
Remote host registry and development Engine connector.
-
Host-aware container list, live filtering, run/create+start, lifecycle actions, action menu, inspect/logs/stats, one-shot exec terminal sessions, and cursor-resumable event stream.
-
Container run options: published ports, environment variables, restart policy, and labels, with client-side validation before create+start.
-
Live image pull progress through gateway operation polling (determinate progress and status messages), and prune operations for containers, images, volumes, and networks from their resource screens. Pulls can be cancelled while running, and every mutation operation (pull, prune, container, image, volume, and network actions) can be cancelled the same way: the gateway aborts the Engine request and reports the operation as
cancelledwithout marking the remote host offline. -
Live image inspect/remove, volume create/inspect/delete, and network create/inspect/delete flows with destructive-action confirmation.
-
Quick search across live remote resources, event notification center, toast feedback, persistent client preferences, Windows login-item integration, resizable terminal drawer, command history, copy, and clear actions.
-
A startup update check against public Harbor Desk GitHub Release metadata, with stable/preview channel selection, a manual status-bar check, and an explicit release-page action. It never downloads or installs a binary.
-
Live gateway/host Troubleshoot diagnostics and a redacted About diagnostic summary.
-
Operation records with idempotency keys for container mutations, audit metadata for implemented mutation paths, and short-lived WebSocket tickets. metadata for implemented mutation paths, and short-lived WebSocket tickets.
-
An admin-only Audit log screen that renders the gateway audit trail with actor, host, action, resource, and result states, and a clear permission-denied state for non-admin users.
-
Generic OIDC provider selection, Authorization Code + PKCE browser launch, server-side token exchange, and Electron keychain refresh-token storage.
-
Remote-native settings and connection/status states.
-
Docker-socket-free renderer boundary enforced by Electron preload.
PostgreSQL/Redis persistence, Vault/KMS secret storage, BullMQ processors for BuildKit/Compose/export/scan, host-grant persistence, and the remaining Kubernetes/registry/extension/AI adapters are intentionally still separate implementation work. Their screens show an explicit unavailable state rather than fixture data or fake success. The Local Gateway wrapper currently keeps host registrations, encrypted development secrets, and operations in memory, so they reset when the app fully quits; a Server Gateway owns its own durable deployment state.
GitHub Release assets and the public npm registry are separate distribution
channels. Check the registry before assuming an unpinned npm exec command
matches the newest GitHub release:
npm view harbor-desk version
npm view harbor-desk dist-tags --jsonStable releases use npm's latest dist-tag. GitHub prereleases are published
under the preview dist-tag so an unpinned command does not unexpectedly move
to a preview. To use an exact published preview version:
npm view harbor-desk@0.6.1 version
npm exec --yes harbor-desk@0.6.1 -- --versionGitHub Release assets and npm packages are published separately. Each
generated release note records the npm version and dist-tag observed after the
tagged package is published. If the requested version is not yet available in
the registry, download the matching harbor-desk-<version>.tgz and
SHA256SUMS assets, verify the checksum, and invoke the tarball explicitly:
npm exec --yes --package ./harbor-desk-<version>.tgz -- harbor-desk --versionThe registry-backed npm command is the server-side entry point. Run it inside an interactive SSH session on the Docker host:
npm exec --yes harbor-desk
npm exec --yes harbor-desk -- install
npm exec --yes harbor-desk -- install-server
npm exec --yes harbor-desk -- --open-release
npm exec --yes harbor-desk -- -AIThe default command, install, and install-server commands open the same
keyboard-driven TUI when they are run with a TTY. No browser is opened on the
server. Use arrow keys or j/k, press Enter to select, and press Ctrl+C to
cancel. The wizard uses a safe directory and port default, detects the local
Docker socket as the common path, lets you choose remote Engine mTLS when
needed, validates the selected connection, and asks for confirmation before
writing files or starting containers:
Harbor Desk server setup
Docker Engine connection
> This server's Docker socket (recommended)
Remote Docker Engine over HTTPS + mTLS
↑/↓ or j/k to move · Enter to select · Ctrl+C to cancel
At the end it prints the connection details. In the default loopback mode this includes the SSH tunnel command and the client URL to use after opening the tunnel. The ordinary desktop client still does not access a local Docker socket; only the server-side Gateway does.
If stdin or stdout is not a TTY, the command fails with SSH guidance instead
of waiting for input. Use explicit install-server options for CI and other
non-interactive environments. The command does not install or launch the
Electron desktop application on the server.
The normal desktop flow accepts either an already running Server Gateway or a Docker Engine endpoint. A raw Engine target is wrapped by a Local Gateway in the desktop process; a Server Gateway is never duplicated locally. On a controlled Linux, Windows, or macOS Docker host, the TUI installs a dedicated external preview gateway by copying the gateway payload, creating a per-install server secret, building the gateway, and starting it on a loopback or explicitly selected network binding. The installer requires an empty target directory and refuses to overwrite an existing install or use an occupied port.
-AI (also accepted as --ai-context) prints stable JSON describing the
commands, defaults, platform support, and security boundary without reading
Docker or touching the filesystem:
npm exec --yes harbor-desk -- install-server -AIThe examples below use the npm-published package. When the GitHub release is
newer than the registry, replace the npm exec --yes harbor-desk -- prefix with
npm exec --yes --package ./harbor-desk-<version>.tgz -- harbor-desk after
verifying the downloaded tarball.
npm exec --yes harbor-desk -- install-server \
--directory /srv/harbor-desk-preview \
--port 4311 \
--engine-name "server local Docker Engine" \
--allow-local-engine-socketUse --dry-run to validate Docker Compose access, the Docker socket, target
directory, authentication configuration, and published port without creating
files or containers. The default is loopback plus development authentication:
npm exec --yes harbor-desk -- install-server \
--directory /srv/harbor-desk-preview \
--port 4311 \
--allow-local-engine-socket \
--dry-runLoopback preview installs include the opaque Origin: null used by a packaged
Electron renderer, in addition to the local Vite origins. The installer refuses
that origin for a public bind; public deployments must provide an explicit
HTTPS client origin instead.
For a network-reachable preview, opt in explicitly. Public binding refuses development authentication and requires a non-empty OIDC provider JSON array; provider endpoints must use HTTPS. The browser origin must be supplied for the client that will call the gateway:
npm exec --yes harbor-desk -- install-server \
--directory /srv/harbor-desk-public \
--public \
--auth-mode oidc \
--oidc-providers-file ./oidc-providers.json \
--allowed-origin https://client.example.com \
--allow-local-engine-socket--public only changes the published port binding to 0.0.0.0; it is not a
production deployment or a substitute for TLS. Put the preview behind a
TLS-terminating reverse proxy and a firewall, restrict --allowed-origin to
origins you control, and protect the provider file. The install plan reports
the public warning but never prints provider credentials. The remote Engine mTLS
options below protect the gateway-to-Engine connection; they do not terminate
client-to-gateway TLS or replace a public reverse proxy.
To connect the server gateway to a remote Docker Engine without mounting the server Docker socket, pass an HTTPS Engine endpoint and all three mTLS files:
npm exec --yes harbor-desk -- install-server \
--directory /srv/harbor-desk-remote \
--engine-endpoint https://engine.example.com:2376 \
--engine-ca-file /etc/harbor-desk/engine/ca.pem \
--engine-cert-file /etc/harbor-desk/engine/client-cert.pem \
--engine-key-file /etc/harbor-desk/engine/client-key.pemThe installer validates that each file is a non-empty regular file, keeps the
source files on the server host, and bind-mounts them read-only at
/run/harbor-desk/engine inside the gateway container. The generated
.harbor-desk.env contains only the source paths and is owner-readable; the
certificate contents are not copied into the install directory, emitted in the
AI context, or returned to a client. Remote Engine mode and
--allow-local-engine-socket are mutually exclusive.
install-server is a development preview installer, not a production
control-plane installer. It uses the documented server-local Engine overlay;
the explicit socket acknowledgement is required because Docker socket access
is highly privileged even with a read-only bind mount. The generated
.harbor-desk.env is created with owner-only permissions and is never printed.
On a Windows or macOS host running Docker Desktop, pass a native destination
path instead. The Engine socket stays /var/run/docker.sock on every platform,
because Docker Desktop resolves that bind source inside its own Linux virtual
machine rather than on the host filesystem.
npm exec --yes harbor-desk -- install-server --directory C:\harbor-desk-preview --port 4311 --allow-local-engine-socket| Component | Linux | Windows | macOS |
|---|---|---|---|
| Desktop client + adaptive Gateway wrapper | AppImage, deb | NSIS installer | dmg, zip (x64 and arm64) |
Optional server gateway (install-server) |
Docker Engine or Docker Desktop | Docker Desktop | Docker Desktop |
The Local Gateway wrapper is bundled with the desktop client and starts only when the configured target is a raw Docker Engine; it does not start or install Docker. A configured Server Gateway is used directly. The optional server gateway runs as a container on any host with Docker Compose, so the same command works against a native Linux Engine and against Docker Desktop. The client never requires a local Docker Engine on any platform.
Release artifacts are built by the Release workflow on ubuntu-latest,
windows-latest, and macos-latest. They are unsigned unless the build
environment supplies signing material, so macOS Gatekeeper and Windows SmartScreen
will warn. Do not treat an unsigned artifact as a trusted production release.
The desktop checks the public Harbor Desk release list once after startup by
default. The request is an unauthenticated metadata-only GET from the Electron
main process to api.github.com; no Docker endpoint, gateway token, host name,
or client preference is sent. The request's User-Agent identifies Harbor Desk
and its current semantic version. Settings → General can disable automatic
checks or exclude preview releases. The status bar always provides a manual
check.
When a newer semantic version is available, the renderer receives only the
current version, newer version, check time, status message, and a release-page
state. Choosing View release asks the main process to open the fixed
github.com/turin-dev/harbor-desk/releases/tag/... page. Harbor Desk does not
read asset download URLs, download packages, run an installer, or replace the
current application automatically. Users must review release notes, signatures,
and SHA256SUMS before choosing to install an artifact.
Electron renderer -- HTTP / WebSocket --> Server Gateway --> Docker Engine
or Local Gateway wrapper
no Docker SDK per-launch token only for wrapper selected host
no Docker socket 127.0.0.1 dynamic port connector boundary
The Electron main process detects the configured target before creating the window. A Server Gateway is used directly without starting a desktop Gateway. When the target is a raw Docker Engine, the main process starts a Local Gateway wrapper on an OS-assigned loopback port. The wrapper accepts API calls only with a random token generated for that app launch; the preload exposes only a narrow getter so the renderer can attach the token to wrapper requests. The token is not written to diagnostics or logs. Closing the window to the tray leaves the app and any Local Gateway wrapper running; choosing Quit stops the wrapper.
The renderer is a control-plane client only. It never receives a Docker socket or talks directly to an Engine. The gateway authenticates each request, applies host authorization, records audit metadata, and makes the selected Engine call. A host selector in the desktop application is not a security boundary.
Connection detection first checks HTTP(S) targets for the Harbor Desk Gateway
health and auth-provider endpoints. If the target is not a Gateway, local
loopback Engine endpoints, npipe:, and unix: are allowed for development;
remote raw Engines must use HTTPS with CA, client certificate, and private key.
VITE_GATEWAY_URL is only an optional first-target seed for development builds;
it does not reserve a local port or imply that a Gateway should start.
Prerequisites:
- Node.js 22 or newer
- pnpm 11.18.0 (Corepack is supported)
- Docker is optional for renderer work; it is needed only when running the local service dependencies or a gateway that connects to an Engine
From the repository root, run the safe bootstrap script for your platform. The scripts install dependencies and create .env from .env.example only when .env does not already exist. They do not start Docker, open ports, or deploy Compose services.
.\setup.ps1bash setup.shFor the full Electron development flow, install dependencies and launch the
desktop. Configure a target in Settings, or provide VITE_GATEWAY_URL as the
first-target seed:
pnpm install
Copy-Item .env.example .env
pnpm --filter @harbor/desktop dev:electronRunning only the Vite renderer in a normal browser has no Electron preload, so
that browser-only workflow still needs a separately started development
Gateway and VITE_GATEWAY_URL:
pnpm --filter @harbor/gateway dev
pnpm --filter @harbor/desktop devThe soak command verifies that the gateway and renderer stay reachable for eight hours. Pass the Electron main-process ID when checking a running desktop window as well; a missing process is recorded as a failed check.
$env:SOAK_DESKTOP_PID = "<Electron main-process ID>"
pnpm run soak:8hUse SOAK_PROCESS_PIDS with a comma-separated list when other long-running processes must be checked in the same run. The default check interval is 30 seconds and can be shortened only for a local smoke test with SOAK_INTERVAL_MS.
To create a desktop build, run the command for the platform you are building on. The default package needs no separate gateway configuration because it includes the managed loopback runtime. electron-builder builds each target on its own operating system, so run these on the matching host:
pnpm --filter @harbor/desktop package:dir
pnpm --filter @harbor/desktop package:winThe Windows target is a branded, bilingual English/Korean assisted NSIS
installer. It preserves the existing upgrade identity, lets the user choose a
per-user or all-users install and destination, creates Start menu and desktop
shortcuts, and launches the client only when the finish-page option remains
selected. The installed application itself always runs with asInvoker; only
an explicitly selected all-users installation may request elevation.
Installer artwork is generated from
scripts/generate-installer-assets.py and checked in under
apps/desktop/build/. See apps/desktop/build/README.md before changing the
fixed NSIS GUID or branding resources.
pnpm --filter @harbor/desktop package:linux
pnpm --filter @harbor/desktop package:macThe installer is unsigned unless the build environment supplies a signing certificate through the electron-builder signing configuration. Do not publish an unsigned artifact as a trusted production release.
For a standalone/server development connector, set DEV_ENGINE_HOST to a
protected Engine endpoint. The Server Gateway or Local Gateway wrapper reads
this value; the renderer does not. Do not expose an unauthenticated Docker
daemon in production, and do not point a Local Gateway wrapper at an untrusted
endpoint.
The optional infra/compose/docker-compose.preview.local-engine.yml overlay is a server-side development fixture. It can bind the host Docker socket into the gateway container so the gateway can connect to a Docker Engine on the same server. It is not a client-local Engine fallback.
For a remote server, use the infra/compose/docker-compose.preview.remote-engine.yml
overlay through the four --engine-* options instead. It keeps Docker Engine
access inside the gateway and uses HTTPS plus CA/client certificate/client key
mTLS without mounting a server Docker socket.
A bind mount marked read-only protects the socket file mount, not the Docker
API. A process that can use that socket can generally perform highly privileged
Docker operations on the server. Use this overlay only on a controlled
development machine or private preview host. Do not publish the socket or treat
the overlay as production isolation. If the gateway itself is made network-
reachable with install-server --public, the installer requires OIDC, but the
preview still needs TLS/reverse-proxy termination, firewall rules, narrow CORS
origins, and operational monitoring before any external users are allowed to
reach it.
The base preview Compose file does not mount a Docker socket and binds its gateway port to loopback by default. The installer can change only the published gateway bind host; the gateway still listens on its internal container port. Compose configuration can be reviewed without starting any service:
$env:SECRET_MASTER_KEY = "replace-with-a-unique-local-value"
$env:DEV_ENGINE_HOST = "unix:///var/run/docker.sock"
docker compose -f infra/compose/docker-compose.preview.yml -f infra/compose/docker-compose.preview.local-engine.yml configThe checked-in .env.example and Compose defaults are development examples, not credentials for a deployment. Never commit a real .env file, certificate, private key, OIDC client secret, access token, or Docker endpoint credential. The reference Compose services bind published ports to loopback to reduce accidental network exposure.
For production, use client-to-gateway TLS (normally at a reverse proxy), AUTH_MODE=oidc, an explicit Engine hostname allowlist, HTTPS Engine endpoints with server-side mTLS material, and an injected Vault/KMS-backed secret store. PostgreSQL/Redis persistence, a production Vault/KMS adapter, durable host grants, workers, and the remaining adapters are not complete in this vertical slice. Do not describe it as production-ready until those acceptance checks are implemented and operated.
- apps/desktop — Electron main process, preload boundary, and React renderer
- apps/gateway — Fastify API, authorization, audits, and Engine connector
- apps/worker — background-worker entry point
- packages/ — shared contracts, configuration, Engine client, and UI
- infra/ — schema migrations and reference Compose files
- docs/operations/ — architecture and security-boundary notes
Read CONTRIBUTING.md before opening a change and SECURITY.md before reporting a vulnerability. The contributor guide includes validation commands and the boundary that must remain intact: the renderer must never acquire direct Docker Engine access.
Report a vulnerability through private security advisories, not a public issue. Release-by-release changes are recorded in CHANGELOG.md, and expected participant behavior is described in CODE_OF_CONDUCT.md.
Apache-2.0. See LICENSE for the license text and NOTICE for attribution.
Harbor Desk is an independent project. It is not affiliated with, endorsed by, or derived from Docker, Inc. "Docker" is a trademark of Docker, Inc., used here only to describe Docker Engine API compatibility.