Skip to content
v0.9.1· Open Source · AGPL-3.0

Remote Docker
Agent

We built Portwing to give Drydock a secure foothold on every host. Pair it with sockguard and it never touches the raw Docker socket. Ed25519 auth proves every request, and a structured audit log records everything it touches for export to immutable storage, so you control your fleet without exposing it.

License AGPL-3.0CIOpenSSF ScorecardGo Report CardGo Reference
GHCR imageMulti-arch: amd64 | arm64 | arm/v7Image size ~10 MBGitHub stars

In the real CLI

See it work

A looping recreation of the real CLI — boot the agent in standard and edge mode and watch it stream structured audit logs.

portwing — zsh
# Standard mode: controller connects inbound. Boot the agent and watch the audit stream.
$

A hand-rendered recreation of the real CLI — every frame mirrors what portwing actually prints. Use the controls above to pause, restart, or change speed.

What it does

14 reasons your Docker hosts are safer

Security isn't a checkbox — it's every layer. Here's what we built.

⌘K
Security

Default-Deny Socket

Pair us with sockguard and we never touch the raw Docker socket. The agent talks to a filtered unix socket instead, so even a fully compromised agent is constrained to an explicit Docker API allowlist enforced at the socket level.

Ed25519 Per-Client Auth

Signed requests with nonce-based replay protection and no shared secrets on the wire. Enroll client public keys into an authorized_keys file, rotate with SIGHUP, and every enrollment is audited.

Argon2id Token Hashing

Prefer a shared secret? Store only its Argon2id hash via TOKEN_HASH. Generate one with `portwing hash-token`; comparison is constant-time. Plaintext TOKEN is supported for evaluation but the hash is the recommended posture.

Signed & Verifiable

Every release ships cosign signatures, per-archive SBOMs, an image SBOM attestation, and SLSA build provenance. Verify the image before you ever trust it with your host — the supply chain is auditable end to end.

Hardened Runtime

We ship hardened by default: `read_only`, `cap_drop: ALL`, `no-new-privileges`, and tokens mounted as secrets instead of env vars. The image runs as non-root (UID 65532) out of the box; running as root (`user: "0:0"`) is an explicit opt-in for socket-group compatibility. No security theater.

Control

Drydock-Native

We speak Drydock's wire protocol (portwing/1.0 over WebSocket in edge mode; dd:* SSE events in standard mode) with full watcher-snapshot streaming, letting one Drydock controller manage containers across every host we're deployed on.

Standard & Edge Modes

Run in standard mode behind a TLS reverse proxy (the controller dials in), or use production-supported outbound edge mode for hosts with no inbound port. The stable portwing/1.0 path is gated by real multi-agent reconnect, exec, log-stream, and backpressure soak tests.

Generic REST Adapter

Don't run Drydock? We work standalone too. Our REST + Bearer-auth surface lets any client list, inspect, start, stop, and stream logs from containers. No controller required.

Read-Only MCP Server

We ship a built-in Model Context Protocol server that exposes host and container state to MCP-aware agents. Read-only by design, so your assistant can see everything and change nothing.

Operations

Prometheus Metrics

Our zero-dependency `/metrics` endpoint exports host/container telemetry plus edge connection, reconnect, backpressure, and audit health. Explicit `/health` and `/ready` probes keep dependency outages from becoming restart loops.

Structured Audit Log

Every action is recorded with a stable structured schema: who asked, what ran, and what the daemon answered. A cursor-based NDJSON path detects overwritten history and restarts before exporters ship records to immutable storage.

Tiny & Multi-Arch

We ship as a single static Go binary, ~10 MB, with zero runtime dependencies. amd64, arm64, arm/v7 — runs on the Raspberry Pi at the edge as happily as the server in the rack.

OpenAPI 3.1 Spec

We publish the full agent API as a machine-readable OpenAPI 3.1 spec. Generate clients, drive contract tests, and never guess at the wire format.

Watchtower Migration

Coming from Watchtower? We wrote a migration guide that maps the update-on-a-host workflow onto Portwing + Drydock, so you trade a cron-like updater for an auditable, controllable agent.

14 of 14 capabilities
↑↓ navigateesc close

Get running

Get started in minutes

Add Portwing to your compose file and point Drydock at it — one agent per host.

Terminal
$ docker run -d \
  --name portwing \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -p 3000:3000 \
  -e TOKEN=$(openssl rand -hex 24) \
  ghcr.io/codeswhat/portwing:latest

TOKEN is visible in docker inspect — fine for a local try, not for production.

On the horizon

Roadmap

Where we've been and where we're headed. Direction, not commitments.

ReleasedCurrent (HEAD)Planned
roadmap
v1.0🚀Stable Release

Stable HTTP API and environment-variable surface under semantic-versioning guarantees

Stable agent ↔ controller wire protocol — no breaking changes without a major bump

+1 more

Threat model review, CVE-clean base image, published security policy at security@codeswhat.com

HEAD -> v0.9.0🔄Controller-Owned Updates

Drydock watcher/update execution runs controller-side through Portwing's authenticated Docker proxy in Standard and Edge modes

Watcher descriptor declares transport=docker-api, execution=controller, and events=portwing; no remote trigger is advertised

+3 more

Edge sends component ownership before raw inventory; full feature compatibility requires Drydock v1.6.0-rc.11+ while DrydockCompat remains 1.4.0

Edge mode shares the cursor-based NDJSON audit exporter on its explicitly private, unauthenticated operations listener

Exec and Docker proxy failures preserve bounded daemon/sockguard error details through the controller path

v0.8.0🧭Toward v1.0

Edge mode graduated to production support — enabled by default in Drydock 1.6 and gated by a real cross-repo multi-agent load/soak

Published stability policy — semver guarantees for the HTTP API, env-var surface, MCP tools, and DrydockCompat wire contract

+3 more

Package-manager distribution — a Homebrew tap and apt/rpm packages via the existing GoReleaser pipeline

Continuous edge live-tail under the dd: namespace, with cancellation, stdout/stderr framing, and bounded backpressure

Mode-aware liveness/readiness, edge/backpressure metrics, stable cursor-based NDJSON audit export, and runnable observability examples

v0.7.0🛡️Fail-Closed Security

Standard mode fails closed without credentials — unauthenticated development requires explicit loopback and remote opt-ins

Ed25519 HTTP signature v2 binds the escaped path and exact raw query; legacy signatures are query-free only

+5 more

Cold Argon2id work is capped agent-wide and per IP; raw-token verifiers retain fixed-size digests

Credential files require regular-file descriptors and safe Unix permissions, checked after open

Enrollment bodies are capped and abuse-limited; Compose uploads use root-confined, symlink-resistant writes

Edge request correlation, bounded log-follow options, and terminal hello-rejection classification

Generated CSP and browser security headers cover every exported website and docs route

v0.6.0🔐Non-Root & Cross-Repo Parity

Agent info surfaced in the Drydock UI — memoryGb, logLevel, and pollInterval reported on every dd:ack

Edge-mode container deletion — dd:container_delete_request/response over the WebSocket tunnel

+4 more

Container environment variables in RuntimeDetails, parsed from Docker's Config.Env

COMPATIBILITY.md — canonical cross-repo version matrix for Portwing × Drydock × sockguard

BREAKING: container image now runs as non-root (UID 65532) by default — root is an explicit opt-in

CI egress lockdown — harden-runner egress-policy: block across all jobs in every workflow

v0.5.0🔒Security Hardening

Pre-auth body limits tightened — oversized requests blocked before auth runs

Registry-auth and proxy query parameters validated before reaching the daemon

+5 more

Private-key file-permission parity — startup fails on world-readable keys

Consistent outbound TLS posture across all HTTP client paths

Goroutine-lifecycle cleanup on shutdown — no leaks under SIGTERM

Benchmark tracking: hot-path ns/op + allocs/op kept as a 90-day artifact

Cosign-signed images, SBOMs, and build provenance on every release

v0.3.0🌐Edge Mode

Edge mode: outbound WebSocket tunnel to Drydock — no inbound port required

Ed25519 per-request signatures with nonce LRU replay protection

+5 more

Ordered exec I/O — input that arrives before the exec is up is buffered and replayed

Outbound backpressure: bounded write queue, per-frame deadline, slow-consumer eviction

Reproducible base images: Dockerfile and Dockerfile.release pinned by digest

Three-tier fuzz harness: 60s PR smoke, 5m nightly, 1h monthly deep pass

Weekly soak test: resident-set budget under sustained SSE+proxy+exec load

v0.1.0🛡️Foundation

Standard mode: HTTP/SSE inbound server, Docker Engine API proxy

Token authentication — plaintext, file-based, and Argon2id hash-at-rest

+5 more

Docker Compose stack lifecycle (up / down / pull / ps)

Exec tunnel: interactive container sessions streamed over HTTP

Structured JSON audit log with in-memory ring buffer and pull endpoint

Health endpoint (/_portwing/health) for Docker HEALTHCHECK directives

Multi-arch image: linux/amd64 + linux/arm64, read_only + cap_drop:ALL runtime defaults

(initial commit)

Community

Star History

Growing every week — join us on GitHub.

Star History Chart

Why Portwing

How we compare

A quick look at what Portwing ships that Portainer Agent, Hawser, and Komodo Periphery don't.

FeaturePortwingPortainerHawserKomodo
Remote container controlYesYesYesYes
Per-client key auth (Ed25519)YesPartialPartialPartial
Default-deny socket filterYesNoNoNo
Structured audit logYesPartialNoNo
Signed images + SBOMYesNoNoNo
MCP server (AI-native)YesNoNoNo
Yes Partial No

Ecosystem

Part of the CodesWhat stack

Portwing is one piece of a small, focused toolkit — each tool does one job, and they compose.

FAQ

Frequently asked questions

Common questions about how Portwing works and how to deploy it securely.

What is Portwing and how does it relate to Drydock?

Portwing is a lightweight Go agent that gives Drydock a secure foothold on a Docker host. Drydock is the orchestrator — it manages container update policies, schedules checks, and sends update actions. Portwing is the agent it talks to on each host: it sits next to the Docker socket, proxies the Docker Engine API to Drydock, and handles container lifecycle actions on request. Without an agent on each host, Drydock can only reach hosts it has a direct network path to. Portwing adds two connection options: standard mode (Drydock connects inbound to the agent's HTTP port) and edge mode (the agent dials out to Drydock over a WebSocket, so NAT and firewalled hosts work too).

What are standard mode and edge mode, and which should I use?

Standard mode runs an HTTP/SSE server on port 3000. The Drydock controller connects inbound, calls the agent's inventory endpoints, and holds a long-lived SSE stream for real-time events. Use it when the host is reachable from your Drydock instance. Edge mode flips the control direction: the agent dials out to Drydock over the stable portwing/1.0 WebSocket tunnel, so no inbound control port is published. Its unauthenticated local operations listener must remain private. Use Drydock v1.6.0-rc.11+ with Portwing v0.9.0 for the complete controller-owned watcher/update feature path.

How does Ed25519 authentication work and why use it over a shared token?

Ed25519 gives each client a distinct cryptographic identity. Every version 2 request carries a detached signature over the HTTP method, escaped path plus exact raw query string, a SHA-256 body hash, a Unix timestamp, and a 128-bit random nonce. The agent verifies the signature, rejects requests with a timestamp skew greater than 60 seconds, and tracks nonces in an in-memory LRU to block replays within the window. No shared secret is transmitted. Compromising one client's private key leaves all other enrolled clients unaffected. By contrast, a shared token (TOKEN or TOKEN_HASH) is a single credential — rotate or revoke it and every caller is disrupted at once. Ed25519 is the recommended production option; token auth is still supported for evaluation and staged migration. Portwing ships a keygen subcommand: run portwing keygen to generate a keypair and get the authorized_keys line to drop on the agent host.

Is it safe to expose Docker control through Portwing? What's the security model?

Portwing is not a firewall — it is an authenticated transport. Out of the box it authenticates callers and proxies Docker API requests to the daemon. If you want a socket-level allowlist that blocks individual Docker API paths and request body properties (privileged mode, host mounts, capability additions), pair Portwing with sockguard: sockguard writes a filtered unix socket into a shared volume, and Portwing talks to that filtered socket instead of the raw /var/run/docker.sock. The recommended compose file ships with this two-layer configuration pre-wired. All official images run with read_only: true, cap_drop: ALL, no-new-privileges, and Docker secrets instead of inline tokens. Edge mode publishes no inbound control port, but its local operations listener exposes metrics and audit export without inbound auth and must stay on a trusted network.

What does the audit log record and how do I enable it?

Set AUDIT_LOG to a file path, stdout, or stderr. The agent writes one JSON record per line for every security-relevant event: authenticated API calls (api_request), failed auth attempts (auth_failure), rate-limit blocks (rate_limited), Compose lifecycle operations (compose_op), exec tunnel opens (exec_start), and Ed25519 key enrollments (enrollment). Every record includes a UTC nanosecond timestamp, the client IP, HTTP method and path, and an outcome (allowed / denied / error). Portwing also keeps an in-memory ring buffer (default 256) with an authenticated Standard-mode JSON view and cursor-based NDJSON export. Edge mode exposes only the NDJSON export on its private operations listener without inbound auth, so restrict that listener to the trusted exporter or monitoring workload.

What platforms does Portwing run on, and how do I install it?

Portwing ships as a multi-arch container image for linux/amd64, linux/arm64, and linux/arm/v7; as signed deb and rpm packages for those architectures; and as a Homebrew cask for Intel and Apple Silicon Macs. Docker Compose with sockguard remains the recommended hardened deployment, while native packages are convenient for host-managed services and local CLI use. Stable releases update codeswhat/tap/portwing; prereleases do not move that channel. See https://portwing.codeswhat.com/docs/installation for verification, configuration, upgrade, and uninstall instructions.

Is Portwing production-ready?

Portwing v0.9.x is a supported pre-v1 release, so operators should pin versions and review the changelog and published stability policy. Both standard and edge modes are production supported: edge is gated by a real multi-agent reconnect/load soak with concurrent exec, continuous logs, and controller backpressure. Use Drydock v1.6.0-rc.11+ for full v0.9 watcher/update feature compatibility; the stable wire contract remains DrydockCompat 1.4.0. The public roadmap still targets v1.0 for the first fully stable release. For the strongest deployment posture, pair either mode with sockguard. Report issues at https://github.com/CodesWhat/portwing/issues and security concerns at security@codeswhat.com.

Do I need sockguard? What does adding it actually change?

Sockguard is optional but recommended for production. Without it, Portwing proxies any Docker API request that passes authentication — which includes container creates with privileged mode, arbitrary bind mounts, or capability additions if a caller requests them. Sockguard sits between Portwing and the Docker socket and applies a default-deny allowlist: every Docker API call is blocked unless an explicit rule in sockguard.yaml permits it (method, path, and request body). The bundled sockguard.yaml mirrors the Drydock preset, which covers container lifecycle, image operations, events, and narrow network and volume reads. Even if Portwing were fully compromised, sockguard constrains it to that explicit allowlist. The two-layer docker-compose example in the repo pre-wires both containers with a shared filtered socket.