Warning
Pre-1.0 software — APIs may still change. Portwing is pre-v1.0.0 (currently v0.9.25). The compatibility guarantees that already apply are published in STABILITY.md; other surfaces may still change between minor releases. Pin to an exact version and review the CHANGELOG before upgrading.
Note
v0.9.25 is the current release. It is built with Go 1.27.2, which fixes 13 standard-library advisories; ten of them were reachable from Portwing's code in the previous release, so upgrading is recommended. Binary request uploads require a controller that implements the negotiated edge-request-body-stream capability. Wire compatibility remains portwing/1.0 and DrydockCompat 1.4.0; full watcher/update feature compatibility requires Drydock v1.6.0-rc.11+. See CHANGELOG.md for the full itemized history.
- Documentation
- Quick Start
- Why Portwing
- Features
- Feature Comparison
- Roadmap
- Star History
- Built With
- Community & Support
- CodesWhat Ecosystem
Recommended: the hardened deployment. It combines three controls: sockguard (socket-level request filtering so Portwing never touches the raw Docker socket), Ed25519 authentication (signed requests or the required signed edge hello, with replay protection and no shared secret), and a hardened container runtime (read_only, cap_drop: ALL, no-new-privileges, secrets-mounted credentials). The plaintext examples publish port 3000 only on host loopback. For remote access, either configure Portwing TLS before widening that bind, or keep the plaintext listener private behind a TLS-terminating reverse proxy. Use edge mode when the host must dial out, and Drydock v1.6.0-rc.11+ for the complete v0.9 watcher/update contract.
Step 1 — generate a token and pull the example:
openssl rand -hex 32 > portwing_token.txt
sudo chown 65532:65532 portwing_token.txt && sudo chmod 0400 portwing_token.txt
export DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
# Download the hardened compose file and its sockguard policy
curl -fsSLO https://raw.githubusercontent.com/CodesWhat/portwing/main/examples/docker-compose.with-sockguard.yml
curl -fsSLO https://raw.githubusercontent.com/CodesWhat/portwing/main/examples/sockguard.yamlStep 2 — start the hardened stack:
docker compose -f docker-compose.with-sockguard.yml up -dThis runs sockguard and Portwing as separate containers sharing a filtered socket volume. Neither container has the raw Docker socket mounted directly; sockguard enforces an allowlist of Docker API operations at the socket level.
Full compose file (examples/docker-compose.with-sockguard.yml)
# Portwing + sockguard — two-layer defense.
#
# Sockguard sits between Portwing and the host's Docker socket and writes a
# filtered unix socket into a shared named volume. Portwing talks to that
# filtered socket instead of mounting /var/run/docker.sock directly, so even
# a fully compromised agent is constrained to the explicit API allowlist in
# sockguard.yaml.
#
# Generate a token first and make it readable by the container user:
# openssl rand -hex 32 > portwing_token.txt
# sudo chown 65532:65532 portwing_token.txt && sudo chmod 0400 portwing_token.txt
#
# Both images run as UID 65532. Sockguard needs the numeric group ID of the
# host Docker socket to open it:
# export DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
# Portwing itself needs no group_add here — it talks only to sockguard's
# filtered socket, which sockguard creates 0600 under the same UID.
# This plaintext example publishes only on host loopback. For remote access,
# configure Portwing TLS before changing this bind, or keep the plaintext
# listener private behind a TLS-terminating reverse proxy.
services:
sockguard:
image: ghcr.io/codeswhat/sockguard:latest
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
group_add:
- "${DOCKER_SOCK_GID:?set to the GID of /var/run/docker.sock (see header)}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./sockguard.yaml:/etc/sockguard/sockguard.yaml:ro
- sockguard-socket:/var/run/sockguard
environment:
- SOCKGUARD_LISTEN_SOCKET=/var/run/sockguard/sockguard.sock
portwing:
image: ghcr.io/codeswhat/portwing:0.9.25
restart: unless-stopped
depends_on:
- sockguard
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
user: "65532:65532" # image default; explicit so it survives image overrides
ports:
- "127.0.0.1:3000:3000"
volumes:
- sockguard-socket:/var/run/sockguard:ro
- portwing-stacks:/data/stacks
environment:
- DOCKER_SOCKET=/var/run/sockguard/sockguard.sock
- TOKEN_FILE=/run/secrets/portwing_token
secrets:
- portwing_token
secrets:
portwing_token:
file: ./portwing_token.txt
volumes:
sockguard-socket:
portwing-stacks:The sockguard.yaml preset above (a copy of sockguard's portwing.yaml) denies all exec. If Drydock's edge exec feature is in play, use examples/docker-compose.edge-with-exec.yml instead, which pairs edge mode with sockguard's portwing-with-exec.yaml preset (examples/sockguard-with-exec.yaml). See the Drydock integration notes for how denial reasons reach the controller.
Upgrade to Ed25519 key auth (zero shared secrets): generate a keypair with portwing keygen, mount the authorized_keys file, and set AUTHORIZED_KEYS=/etc/portwing/authorized_keys. Use PRIVATE_KEY_FILE for signed edge-mode hellos. See Authentication.
Edge mode variant (outbound WebSocket — stable portwing/1.0)
Production supported. Edge mode uses the stable
portwing/1.0protocol and is covered by Drydock's cross-repoquality-portwing-fleet-soak.ymlworkflow: real Portwing processes under multi-agent reconnect, exec, backpressure, and continuous-log load. Portwing's separatequality-soak-weekly.ymlcovers the Standard/generic HTTP path and SSE churn under an RSS-growth budget. Use Drydockv1.6.0-rc.11+for full v0.9 watcher/update feature compatibility; older controllers may remain wire-compatible without that behavior.
For hosts behind NAT or a firewall, examples/docker-compose.edge.yml has Portwing dial out to your Drydock controller's edge endpoint (DRYDOCK_URL + /api/portwing/ws); no port is published on the remote host.
Edge mode is Ed25519-only — generate a keypair first and register the public key with Drydock (POST /api/v1/portwing/keys):
portwing keygen -comment "edge-host-01" > portwing_ed25519.pem
sudo chown 65532:65532 portwing_ed25519.pem && sudo chmod 0400 portwing_ed25519.pemservices:
portwing:
image: ghcr.io/codeswhat/portwing:0.9.25
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- portwing-stacks:/data/stacks
environment:
- DRYDOCK_URL=https://drydock.example.com
- PRIVATE_KEY_FILE=/run/secrets/portwing_key
- AGENT_NAME=edge-host-01
secrets:
- portwing_key
secrets:
portwing_key:
file: ./portwing_ed25519.pem
volumes:
portwing-stacks:Native packages (Homebrew, deb, rpm)
Stable releases also ship a Homebrew cask plus signed/checksummed deb and
rpm packages for amd64, arm64, and armv7.
# macOS
brew install --cask codeswhat/tap/portwing
# Debian/Ubuntu (after downloading the matching release asset)
sudo apt install ./portwing_0.9.25_linux_amd64.deb
# Fedora/RHEL (after downloading the matching release asset)
sudo rpm --install ./portwing_0.9.25_linux_amd64.rpmPackages install the command and, on Linux, a hardened portwing.service; they
do not start it before authentication is configured. See the
native installation guide
for artifact verification, configuration, upgrade, uninstall, and service-user
expectations.
Quick start (evaluation only — not for production)
This is for trying Portwing out locally. Environment-variable tokens are visible in
docker inspectand process listings. Do not use in production — use the hardened deployment above instead.
docker run -d \
--name portwing \
--group-add $(stat -c '%g' /var/run/docker.sock) \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 127.0.0.1:3000:3000 \
-e TOKEN=$(openssl rand -hex 24) \
ghcr.io/codeswhat/portwing:0.9.25Portwing now fails closed: Standard mode refuses to start without TOKEN,
TOKEN_HASH, or AUTHORIZED_KEYS. For local-only development, an explicitly
unauthenticated loopback listener requires ALLOW_UNAUTHENTICATED=true and
BIND_ADDRESS=127.0.0.1 (or ::1). A non-loopback unauthenticated listener
additionally requires ALLOW_UNAUTHENTICATED_REMOTE=true; never use that
second override on a shared or production network.
The image runs as the non-root portwing user (UID 65532); --group-add grants it the Docker socket's group so it can reach the daemon.
Binary install (install.sh)
curl -fsSL https://raw.githubusercontent.com/codeswhat/portwing/main/scripts/install.sh | bashThe generated standard-mode config binds to 127.0.0.1. Keep that listener
private behind a TLS-terminating reverse proxy, or configure Portwing TLS before
changing the bind for remote access.
Every release image is cosign-signed. Verify the signature before running Portwing in production: see Verifying Releases. Report vulnerabilities privately through SECURITY.md.
See the Getting Started guide for Docker Compose, TLS, and Sockguard variants. What changed in each release is in CHANGELOG.md and on the GitHub Releases page.
Controlling a remote Docker host usually means exposing the Docker socket, or running an agent that mounts it directly. Portwing is a small static Go agent that sits in front of the daemon instead. It is a transparent Docker API proxy with per-client Ed25519 authentication, fail-closed startup, structured audit logging, and signed releases, and it pairs with sockguard so the agent never touches the raw Docker socket.
It works for Drydock, which connects inbound to a standard-mode agent or accepts an outbound edge-mode tunnel from hosts behind NAT, and it runs standalone with a REST + SSE API when no controller is involved.
flowchart LR
subgraph server ["Your server"]
DD["Drydock<br/>(controller + UI)"]
end
subgraph hostA ["Remote host A"]
direction LR
LA["Portwing<br/>(agent)"]
SGA["sockguard<br/>(socket filter)"]
DA["Docker Engine"]
LA -- "filtered socket" --> SGA --> DA
end
subgraph hostB ["Remote host B"]
direction LR
LB["Portwing<br/>(agent)"]
SGB["sockguard<br/>(socket filter)"]
DB["Docker Engine"]
LB -- "filtered socket" --> SGB --> DB
end
DD -- "HTTPS + SSE · X-Dd-Agent-Secret" --> LA
DD -- "HTTPS + SSE · X-Dd-Agent-Secret" --> LB
The Drydock controller connects inbound to each standard-mode Portwing agent over HTTP/HTTPS (it initiates; Portwing serves). Each agent reaches the Docker Engine only through a sockguard socket filter. In production-supported edge mode, the agent instead dials Drydock over the stable
portwing/1.0WebSocket tunnel, so no inbound control port needs publishing. Full v0.9 watcher/update integration requires Drydockv1.6.0-rc.11+. Keep the separate unauthenticated operations listener private — see Connection Modes.
| Feature | Description | |
|---|---|---|
| 🔀 | Connection Modes | Standard mode lets Drydock connect inbound over HTTP/SSE. Production-supported edge mode lets the agent dial outbound over the stable portwing/1.0 WebSocket tunnel for NAT/firewalled hosts; full v0.9 watcher/update support requires Drydock v1.6.0-rc.11+. |
| 🔁 | Transparent Docker API Proxy | All Docker Engine API paths forwarded to the local daemon — streaming endpoints, exec session hijacking, and long-lived connections included. |
| 🔑 | Ed25519 Per-Client Authentication | Per-request signatures with per-client keys, replay protection via nonce LRU and timestamp window, authorized_keys-style rotation via SIGHUP, zero shared secrets. |
| 🔒 | Argon2id Token Hashing | Hash your token at rest with OWASP-recommended Argon2id parameters; TOKEN_HASH_FILE for Docker secrets support; SHA-256 success cache keeps per-request overhead flat. |
| 🤖 | MCP Server | AI assistants connect to /_portwing/mcp (Streamable HTTP, protocol revisions 2026-07-28 and 2025-11-25). Read-only tools: list_containers, inspect_container, container_logs, host_metrics, container_stats. Env variable values are never transmitted. |
| 📦 | Container Inventory | Full container metadata with dd.* label parsing and SSE broadcasting. Portwing marks watcher execution as controller-owned so compatible Drydock runs native watcher/update calls through the Standard or Edge Docker proxy. |
| 📈 | Prometheus Metrics | Host and per-container CPU/memory/network in cAdvisor-compatible format at /_portwing/metrics. Zero external dependencies. |
| 📜 | Audit Logging | Structured JSON of every authenticated API call, auth event, exec session, and Compose operation. Recent records are retained in memory by default; file/stdout/stderr persistence is opt-in. |
| 🖥️ | Host Metrics | CPU, memory, disk, network, and uptime collection. |
| ⌨️ | Interactive Exec | Terminal sessions via WebSocket or HTTP hijack with a default cap of 100 concurrent sessions. |
| 🗂️ | Docker Compose | Full lifecycle management with security hardening — path traversal protection, env var denylist, service name injection prevention. |
| 📡 | SSE Compatibility | Drop-in replacement for existing Drydock agents, including dd:watcher-snapshot full inventory on connect. |
| ✍️ | Signed Supply Chain | Cosign keyless signatures, per-archive CycloneDX SBOMs, an image SBOM attestation, and SLSA Build L2 provenance on every release. Verifiable without managing signing keys. |
| 🛡️ | Two-Layer Defense | Pair with sockguard so the agent never touches the raw Docker socket directly. |
| 🪶 | Minimal Footprint | Static Go binary (~10 MB). Compressed container image: ~45 MB amd64 and ~41 MB arm64 (Wolfi, Chainguard), ~33 MB arm/v7 (Alpine). CGO disabled, stripped, no external runtime dependencies. |
| 🧩 | Standalone Mode | ADAPTER=generic provides a clean REST + SSE API on /api/v1/* backed by the local Docker daemon — no Drydock account required. |
How does Portwing compare to other remote Docker agents?
✅ = supported ❌ = not supported
⚠️ = partial / limited ? = not documented or not evaluated † = archived, no longer maintained
| Feature | Portwing | Portainer Agent | Komodo Periphery | Arcane Agent | Hawser |
|---|---|---|---|---|---|
| Transparent Docker API proxy | ✅ | ✅ | ❌ | ❌ | ✅ |
| Inbound (controller-to-agent) connection | ✅ | ✅ | ✅ | ✅ | ✅ |
| Outbound edge connection | ✅ | ✅ | ✅ | ✅ | ✅ |
| Per-request signed HTTP authentication | ✅ | ? | ? | ? | ? |
| Optional mTLS for the agent link | ❌ | ? | ✅ | ? | |
| Default-deny socket filter in the documented deployment | ✅ (with Sockguard) | ❌ | ❌ | ❌ | |
| Agent-level structured audit log | ✅ | ||||
| Prometheus scrape endpoint on the agent | ✅ | ? | ? | ? | ? |
| Read-only MCP server | ✅ | ? | ? | ? | ? |
| Signed release evidence (cosign, SBOM, provenance) | ✅ | ||||
| Fleet UI and controller workflows | ❌ | ✅ | ✅ | ✅ | ? |
| License | AGPL-3.0 | Zlib (agent) / proprietary Business features | GPL-3.0 | BSD-3-Clause | MIT |
Full v0.9 watcher/update behavior needs Drydock
v1.6.0-rc.11+. The edge connection itself works with Drydock 1.6.x, or 1.5.x withDD_EXPERIMENTAL_PORTWING=true; see COMPATIBILITY.md. Portwing has no RBAC, GitOps or Swarm support by design; the fleet product features belong in Drydock.
| Feature | Portwing | Diun | Watchtower † |
|---|---|---|---|
| Remote Docker API proxy | ✅ | ❌ | ❌ |
| Authenticated remote access | ✅ | ❌ | ❌ |
| Structured audit log | ✅ | ❌ | ❌ |
| Default-deny socket filter in the documented deployment | ✅ (with Sockguard) | ❌ | ❌ |
| Read-only MCP server | ✅ | ? | ? |
| Outbound edge / NAT tunnel | ✅ | ❌ | ❌ |
| Image update detection or auto-update | ❌ | ✅ | ✅ |
| Single lightweight Go binary | ✅ | ✅ | ✅ |
| License | AGPL-3.0 | MIT | Apache-2.0 |
Watchtower's upstream project is archived. Diun and Watchtower do update detection; Portwing is the access agent and leaves update decisions to Drydock.
The remote-agent table is compiled from the published competitive landscape, which lists its primary sources and records unknown competitor behavior as "not documented" rather than guessing it absent. Compared versions: Portainer 2.39.5, Komodo Periphery v2.3.2, Arcane Agent v2.10.1, Hawser v0.2.46. Reviewed 2026-08-29; Arcane re-checked 2026-09-02. Portainer and Komodo release evidence checked 2026-10-08. The update-tools table follows the Diun and Watchtower comparison pages, which pin no version or review date. Contributions welcome if any information is inaccurate.
Version themes & highlights
This direction covers at least the next twelve months, through August 2027. High-level themes only; see ROADMAP.md for direction and non-goals and CHANGELOG.md for per-release detail.
| Version | Theme | Highlights |
|---|---|---|
| v0.1.x ✅ | Foundation | Transparent Docker API proxy, standard-mode HTTP server, edge-mode WebSocket tunnel, Drydock adapter, SSE event stream, token auth with timing-safe comparison, rate limiting, multi-arch image |
| v0.2.x ✅ | Security & Observability | Ed25519 per-request auth, key enrollment, Argon2id token hashing, read-only MCP server, Prometheus metrics, structured audit logging, generic REST adapter, cosign keyless signing, OpenAPI 3.1 spec |
| v0.3.x ✅ | Rename & Edge Fixes | Lookout renamed to Portwing, startup banner, GoReleaser dockers_v2 migration, edge reconnect backoff and read-deadline fixes |
| v0.4.x ✅ | Quality Gates | Monthly deep fuzzing, weekly soak test, monthly benchmark tracking, edge tunnel test harness, edge exec input ordering and outbound backpressure fixes |
| v0.5.x ✅ | Hardening | Request and application Prometheus metrics, audit ring buffer and GET /_portwing/audit, Kubernetes examples, pre-auth request body cap, private-key permission check, outbound TLS 1.2 floor, edge mode requires PRIVATE_KEY_FILE |
| v0.6.0 ✅ | Compatibility & Non-Root | Container image runs as non-root UID 65532, COMPATIBILITY.md cross-repo version matrix, edge container deletion, CI egress lockdown |
| v0.7.x ✅ | Fail-Closed Standard Mode | Standard mode refuses to start without credentials, security hardening pass (PW-SEC-001 to 010), edge log and delete request correlation, edge reconnect classification of terminal hello rejections, dead DOCKER_HOST surface removed |
| v0.8.x ✅ | Operations & Distribution | Mode-aware /health and /ready, cursor-based NDJSON audit export, runnable Compose and Kubernetes observability examples, continuous edge logs, Homebrew cask and signed deb/rpm packages, published stability policy, edge mode production supported |
| v0.9.x ✅ | Controller-Owned Updates | Controller-owned Drydock watcher and update execution (Drydock v1.6.0-rc.11+), edge audit export, loopback default for the edge operations listener, MCP revision 2026-07-28, ALLOWED_ORIGINS and ALLOWED_HOSTS browser origin and Host checks |
| v1.0.0 | Binding Stability | STABILITY.md guarantees become binding semver commitments, final re-verify of the competitive review against primary sources, decision on a versioned docs archive. Gated on verifiable items, not a date |
| Post-v1 | Demand-Driven | Controller-managed Portwing upgrade and rollback waves, optional client-certificate authentication, polling/intermittent edge transport, controller-assisted two-key rotation |
SLSA Build L3 isn't tied to a version. It follows an org-shared reusable release workflow landing.
Real-time chat and early support: CodesWhat Discord
Non-security bugs and concrete feature requests go to GitHub Issues; open-ended questions, ideas, and design discussion go to GitHub Discussions. Vulnerabilities must not be filed as public issues — see SECURITY.md for private disclosure. Pull requests are welcome; start with CONTRIBUTING.md.
| Tool | Role |
|---|---|
| drydock | Container update monitoring — web UI and notification engine |
| portwing | Remote Docker agent — secure socket-level access from Drydock or standalone |
| sockguard | Docker socket proxy — default-deny allowlist filter protecting the socket |
These three tools are designed to layer: sockguard filters the socket, portwing exposes it remotely, and drydock monitors and acts on container state.
See COMPATIBILITY.md for the full compatibility matrix across all three tools.