Run a local process as if it were inside your cluster: prefix any command
with plug and it resolves the cluster DNS and reaches the cluster services like
any workload of the stack - no code change, no proxy configuration in your app. One
tiny agent container on the cluster side, one static CLI on yours. Docker, Swarm, Kubernetes
and OpenShift.
Docker, Compose and Swarm
The agent is deliberately boring: a small image holding one static Go binary and no shell at
all, an SSH server with nothing behind it to reach. No state, no volume, no configuration. Its only job is to
be inside the overlay networks and dial services on the CLI's behalf
(direct-tcpip). The
same image runs on Kubernetes too.
In your stack (recommended)
The simplest deployment is no deployment at all: add the service to the stack you want to plug into. It joins that stack's network automatically - nothing else to declare:
Loading plug-service.yml…
To let in only the developers a member accepts, uncomment PLUG_ADMISSION: invite.
On Swarm the agent keeps its members in a Swarm config by itself; on plain Compose, mount the
plug-members volume the comment shows, or the agent refuses to start. How it
works under admission by approval.
Standalone - one agent for several stacks
To cover multiple stacks with a single agent, deploy deploy/plug-stack.yml and list their overlay networks explicitly:
curl -fsSLO https://raw.githubusercontent.com/softwarity/plug/main/deploy/plug-stack.yml
# edit the networks: section, then:
docker stack deploy -c plug-stack.yml plugLoading plug-stack.yml…
Port
2222 is a convention, not a requirement. Publish any port you like and put it in
the profile. In Swarm's routing mesh the agent is reachable via
any node's IP.
Image & tags
| Tag | Meaning |
|---|---|
latest | Last build of main |
x.y.z / x.y / x | Released versions (created by the Release workflow, matching the GitHub Release of the CLI) |
Multi-arch manifest - linux/amd64 and linux/arm64, each built on
native runners (no QEMU). Hosted on
Docker Hub.
It also serves the CLI
The image is multi-stage: it compiles the CLI binaries into /opt/plug/bin/ and
writes /opt/plug/VERSION. The get user serves a fixed, tiny set and
nothing else - the agent version, a named binary (<os>-<arch>),
the wintun driver DLL for Windows, and two installers: install for
Linux/macOS (binaries embedded, picked with uname) and
install-windows (a Git Bash script). The one-liners that call them, per OS,
are on Getting started, where they are kept.
The version baked into the image (and stamped into every binary) is what the
launcher asks for to run the matching version. Released images
carry a real x.y.z; latest carries dev.
The name in the cluster
plug -s <name>:<cluster-port>:<local-port> <cmd> publishes the process
in the cluster under a DNS name, for the lifetime of the session. A dev runs
plug -s service1:8081:4200 npm start and any workload calling
http://service1:8081 lands on their machine's :4200
- no name pre-declared, no redeploy. When the session ends the name is gone.
For that the agent needs to create the DNS name on the fly, which means the Docker socket - already in the snippet above, and required. With it, plug creates the name when the session starts and removes it when the session ends - nothing to pre-declare. Without it, use a name you declared yourself (a network alias on the plug service). Either way, if the name can't be reached plug stops with the reason - never a silent no-op - and one name is served by one session at a time.
The name already belongs to a deployed service - the very one you are developing? plug takes it over: the deployed workload is parked for the session (its containers are stopped; on Swarm the service is scaled to 0) and restored when the session ends - replica count included, and even across an agent restart. Your local process answers the name in its place; afterwards the cluster is exactly as it was. A name held by another live plug session is still refused - takeover applies to deployed workloads only. (Callers holding a cached DNS answer - the JVM caches ~30 s - may see a few connection errors right at the switch.)
Two switch-time facts worth knowing. Web apps: your local build and the deployed one are different builds served under the same URL - their hashed chunks don't overlap, so a browser tab opened on one will fail to lazy-load chunks after a switch. Hard-reload the tab (Cmd-Shift-R) after switching; keep DevTools' “Disable cache” on while developing. Watch mode: each rebuild briefly closes your local port, so in-cluster callers see a few connection-refused during recompiles - normal, it recovers by itself.
On Swarm, run the agent on a manager node - also already set above. One replica is the usual shape and what the snippet deploys, but it is not a requirement: a signpost relays to the agent task holding the session, never to the service VIP. Your existing network needs no change (a non-attachable overlay is fine).
-s. Mount it only on a cluster you trust (the same
trust plug's no-auth transport already assumes).
198.18.x.x range are recognized and discarded), and an absent name
answers unknown host - no more phantom connection refused. Belt and
braces, or on older agents: Docker Desktop → Settings → Docker Engine, add
"dns": ["1.1.1.1", "8.8.8.8"] so unknown names never leave the VM at all.
plug update rolling the agent). A caller that pools
keep-alive connections and caches DNS - a Java gateway, typically - keeps
hitting the old address until its pool recycles, which can take a while. plug is already
answering at the new one: bounce the caller
(docker service update --force your-gateway) and it reconnects instantly.
Installed a launcher before -s existed? Put -s after
-p/--host - older launchers pass flags they don't know to the core.
Under the hood
- Two SSH users:
plug(public-key, runs the tunnel) andget(passwordless,ForceCommand-locked to serving a binary) - see Security model. - Host keys are generated at container start - connections use
StrictHostKeyChecking=no, consistent with the no-auth model. - The
plugaccount may forward: the CLI splices each flow onto an SSHdirect-tcpipchannel, so the agent does the real dials from inside the cluster. - The container logs its attached networks at startup -
docker service logs <stack>_plugis your first debugging stop.