From 82c9efd82192fbbd32d4c91fec2067d67e7b47f8 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:14:32 -0400 Subject: [PATCH 1/6] docs: state the measured compressed image size, not the binary size The ghcr.io/codeswhat/portwing:0.9.23 manifests sum to 44.8 MB (amd64), 40.5 MB (arm64) and 33.4 MB (arm/v7) of compressed layers. The ~10 MB figure is the unpacked amd64 binary (9.4 MB), and ~47 MB matched neither. The Watchtower comparison cell is now containrrr/watchtower:latest at 5.6 MB compressed (amd64) instead of an unsourced ~50 MB. --- README.md | 2 +- docs/content/docs/migrating-from-watchtower.mdx | 4 ++-- docs/migrating-from-watchtower.md | 2 +- website/src/components/github-badges.tsx | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a29de7b5..651a8cac 100644 --- a/README.md +++ b/README.md @@ -368,7 +368,7 @@ flowchart LR | 📡 | **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](https://github.com/codeswhat/sockguard) so the agent never touches the raw Docker socket directly. | -| đŸĒļ | **Minimal Footprint** | Static Go binary, ~47 MB Wolfi (Chainguard) container image. CGO disabled, stripped, no external runtime dependencies. | +| đŸĒļ | **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. |
diff --git a/docs/content/docs/migrating-from-watchtower.mdx b/docs/content/docs/migrating-from-watchtower.mdx index ccdee13d..6846d9e3 100644 --- a/docs/content/docs/migrating-from-watchtower.mdx +++ b/docs/content/docs/migrating-from-watchtower.mdx @@ -29,7 +29,7 @@ Drydock + Portwing invert the model. Portwing runs on each host and exposes the | Container exec / terminal | No | Yes (WebSocket) | | Container filtering | Label `com.centurylinklabs.watchtower.enable` | Label `dd.watch=true` | | Self-hosted server required | No | Yes (Drydock) | -| Image size | ~50 MB | ~10 MB (Portwing) + Drydock server | +| Image size (compressed) | ~6 MB | ~45 MB (Portwing, ~10 MB binary) + Drydock server | Portwing does **not** automatically pull and recreate containers on a schedule. That decision is made by the Drydock controller through its UI and notification pipeline — not by the agent. If you relied on Watchtower's fully autonomous update loop, you will need to configure update policies in Drydock. @@ -211,7 +211,7 @@ For production deployments, upgrade to Ed25519 per-request signing after the ini | Compose support | Recreate only | Full lifecycle (`up`/`down`/`pull`/`ps`/`logs`) | | Exec / terminal | No | Yes (WebSocket) | | Self-hosted required | No | Yes (Drydock server) | -| Resource footprint | ~50 MB image | ~10 MB Portwing + Drydock server | +| Resource footprint | ~6 MB compressed image | ~45 MB compressed Portwing image (~10 MB binary) + Drydock server | See [Configuration](/configuration) for the full environment-variable reference and [Security Model](/security-model) for Portwing's default-deny socket filtering and rate-limiting details. diff --git a/docs/migrating-from-watchtower.md b/docs/migrating-from-watchtower.md index 51a894b8..830e3b2a 100644 --- a/docs/migrating-from-watchtower.md +++ b/docs/migrating-from-watchtower.md @@ -197,7 +197,7 @@ checked for updates by Drydock. | Compose support | Recreate only | Full lifecycle (`up`/`down`/`pull`/`ps`/`logs`) | | Exec / terminal | No | Yes (WebSocket) | | Self-hosted required | No (just Docker) | Yes (Drydock server) | -| Resource footprint | ~50 MB image | ~10 MB Portwing image + Drydock server | +| Resource footprint | ~6 MB compressed image | ~45 MB compressed Portwing image (~10 MB binary) + Drydock server | --- diff --git a/website/src/components/github-badges.tsx b/website/src/components/github-badges.tsx index 620d05a6..8f734be3 100644 --- a/website/src/components/github-badges.tsx +++ b/website/src/components/github-badges.tsx @@ -42,8 +42,8 @@ const distribution: Badge[] = [ }, { href: GHCR_PACKAGE_URL, - src: "https://img.shields.io/badge/image%20size-~10%20MB-informational?logo=docker&logoColor=white", - alt: "Image size ~10 MB", + src: "https://img.shields.io/badge/image%20size-~45%20MB%20compressed-informational?logo=docker&logoColor=white", + alt: "Compressed image size ~45 MB (amd64)", }, { href: `${GITHUB_URL}/stargazers`, From ee398861157c3391b18766e6ab6e978cc96b388e Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:14:39 -0400 Subject: [PATCH 2/6] docs(roadmap): drop the BuildKit Sockguard profile, it already shipped Sockguard's portwing-with-mediated-build.yaml preset (in sockguard since v1.7.0) allows a stock BuildKit docker compose build, and COMPATIBILITY.md already documents it. Remove it from the post-v1 list in ROADMAP.md and the README roadmap table. --- README.md | 2 +- ROADMAP.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 651a8cac..767bb016 100644 --- a/README.md +++ b/README.md @@ -464,7 +464,7 @@ High-level themes only; see [ROADMAP.md](ROADMAP.md) for direction and non-goals | **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 (v0.9.23) | | **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, BuildKit-aware Sockguard profile | +| **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. diff --git a/ROADMAP.md b/ROADMAP.md index 703f2944..16e8abd8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -104,9 +104,9 @@ calendar date: Post-v1 candidates are intentionally demand-driven: controller-managed Portwing upgrade/rollback waves, optional client-certificate authentication, -polling/intermittent edge transport, controller-assisted two-key rotation, and -a BuildKit-aware Sockguard profile. None should expand the base socket policy -or add an unaudited self-update path merely for feature-table parity. +polling/intermittent edge transport, and controller-assisted two-key rotation. +None should expand the base socket policy or add an unaudited +self-update path merely for feature-table parity. ## Non-goals From 88363bfa6c4cce29b424881f17dbfb9eea50f2f7 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:15:36 -0400 Subject: [PATCH 3/6] fix(website): both Watchtower and Diun document Prometheus metrics Watchtower documents an opt-in, experimental /v1/metrics endpoint behind its HTTP API token (containrrr.dev/watchtower/metrics and /arguments). Diun documents an opt-in metrics server on :9090/metrics (crazymax.dev/diun/config/metrics). The Watchtower "No" was wrong, so the row is a tie and the matrix cell is yes. --- website/src/components/compare-matrix.tsx | 2 +- website/src/lib/comparison-route-data/diun.tsx | 2 +- website/src/lib/comparison-route-data/watchtower.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/components/compare-matrix.tsx b/website/src/components/compare-matrix.tsx index 6441b74d..f9fdc676 100644 --- a/website/src/components/compare-matrix.tsx +++ b/website/src/components/compare-matrix.tsx @@ -111,7 +111,7 @@ const TOOLS: Tool[] = [ leastPrivilege: "no", agentAudit: "no", transparentApi: "no", - metrics: "no", + metrics: "yes", mcpServer: "no", }, }, diff --git a/website/src/lib/comparison-route-data/diun.tsx b/website/src/lib/comparison-route-data/diun.tsx index 274657f7..f387cac8 100644 --- a/website/src/lib/comparison-route-data/diun.tsx +++ b/website/src/lib/comparison-route-data/diun.tsx @@ -10,7 +10,7 @@ Auth for remote access|No (local only)|Yes (Ed25519 per-request signing)|self Structured audit log|No|Yes (JSON, built-in)|self Release artifact verification|Not evaluated|Cosign signatures + CycloneDX SBOM + SLSA provenance|self Default-deny socket filter|No|Yes (with sockguard)|self -Prometheus metrics|Yes|Yes|tie +Prometheus metrics|Yes (opt-in)|Yes|tie MCP server (AI-native, read-only)|No|Yes|self Edge / NAT outbound tunnel|No|Yes (Drydock v1.6.0-rc.11+)|self Single lightweight Go binary|Yes|Yes (~10 MB)|tie diff --git a/website/src/lib/comparison-route-data/watchtower.tsx b/website/src/lib/comparison-route-data/watchtower.tsx index e0efbd42..6dd5341c 100644 --- a/website/src/lib/comparison-route-data/watchtower.tsx +++ b/website/src/lib/comparison-route-data/watchtower.tsx @@ -11,7 +11,7 @@ Structured audit log|No|Yes (JSON, built-in)|self Runtime image signature verification|Basic cosign support (newer builds)|No (controller or admission-control responsibility)|competitor Published release evidence|Archived upstream project|Cosign signatures + CycloneDX SBOM + SLSA provenance|self Default-deny socket filter|No|Yes (with sockguard)|self -Prometheus metrics|No|Yes|self +Prometheus metrics|Yes (opt-in, experimental)|Yes|tie MCP server (AI-native, read-only)|No|Yes|self Edge / NAT outbound tunnel|No|Yes (Drydock v1.6.0-rc.11+)|self Single lightweight Go binary|Yes|Yes (~10 MB)|tie @@ -22,7 +22,7 @@ key|Remote Auth (Ed25519)|Watchtower runs locally and has no remote access model shield|Default-Deny Socket Filter|Portwing pairs with sockguard to filter Docker API calls at the socket level. Even if Portwing is compromised, the sockguard allowlist constrains what can be called. Watchtower mounts the socket unfiltered. filetext|Structured Audit Log|Portwing logs every Docker API call it proxies as structured JSON for export to immutable storage. Watchtower has no audit trail. packagecheck|Maintained Signed Releases|Watchtower's upstream repository is archived. Portwing remains actively maintained and every release ships per-archive CycloneDX SBOMs, an image SBOM attestation, cosign image signatures, and SLSA build provenance. This verifies Portwing itself; it is not workload image-signature enforcement. -activity|Prometheus Metrics|Portwing exposes agent health, request counts, and latency histograms. Watchtower has no metrics endpoint. +activity|Prometheus Metrics|Both expose Prometheus metrics. Watchtower's is an opt-in, experimental /v1/metrics endpoint behind its HTTP API token. Portwing's covers agent health, request counts, and latency histograms. bot|MCP Server (AI-Native)|Portwing ships five read-only MCP tools for container and host inspection. Watchtower has no documented MCP support. `, highlightIconMap: { From 547155a00f9e571cb2c3818b07d975d4b330c1b8 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:15:41 -0400 Subject: [PATCH 4/6] fix(website): show "not documented" instead of "no" in the comparison matrix The route pages already say competitor MCP and Portainer Prometheus support is not documented, but the matrix and the home-page teaser rendered a flat "no". Add an undocumented cell value to ComparisonCellIcon (question-mark icon, "Not documented" for screen readers), use it wherever the route data says not documented, and add it to both legends. The Watchtower and Diun route-table MCP cells now match their prose. --- website/src/components/compare-matrix.tsx | 24 +++++++++++-------- website/src/components/compare-section.tsx | 16 ++++++++----- .../src/components/comparison-cell-icon.tsx | 15 ++++++++++-- .../src/lib/comparison-route-data/diun.tsx | 2 +- .../lib/comparison-route-data/watchtower.tsx | 2 +- 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/website/src/components/compare-matrix.tsx b/website/src/components/compare-matrix.tsx index f9fdc676..aea91eea 100644 --- a/website/src/components/compare-matrix.tsx +++ b/website/src/components/compare-matrix.tsx @@ -1,13 +1,13 @@ -import { ArrowUpRight, Check, Minus, X } from "lucide-react"; +import { ArrowUpRight, Check, CircleQuestionMark, Minus, X } from "lucide-react"; import Link from "next/link"; -import { ComparisonCellIcon } from "@/components/comparison-cell-icon"; +import { type CellValue, ComparisonCellIcon } from "@/components/comparison-cell-icon"; import { SITE_CONFIG } from "@/lib/site-config"; // Full comparison matrix for /compare. Each competitor row links to its // dedicated deep-dive page. Cell values are derived from comparison-rows.ts // and the per-competitor data in lib/comparison-route-data/. -type Cell = "yes" | "partial" | "no"; +type Cell = CellValue; const FEATURES = [ { key: "remoteControl", label: "Remote control" }, @@ -55,8 +55,8 @@ const TOOLS: Tool[] = [ leastPrivilege: "no", agentAudit: "partial", transparentApi: "yes", - metrics: "no", - mcpServer: "no", + metrics: "undocumented", + mcpServer: "undocumented", }, }, { @@ -70,7 +70,7 @@ const TOOLS: Tool[] = [ agentAudit: "partial", transparentApi: "no", metrics: "partial", - mcpServer: "no", + mcpServer: "undocumented", }, }, { @@ -84,7 +84,7 @@ const TOOLS: Tool[] = [ agentAudit: "partial", transparentApi: "no", metrics: "partial", - mcpServer: "no", + mcpServer: "undocumented", }, }, { @@ -98,7 +98,7 @@ const TOOLS: Tool[] = [ agentAudit: "partial", transparentApi: "yes", metrics: "partial", - mcpServer: "no", + mcpServer: "undocumented", }, }, { @@ -112,7 +112,7 @@ const TOOLS: Tool[] = [ agentAudit: "no", transparentApi: "no", metrics: "yes", - mcpServer: "no", + mcpServer: "undocumented", }, }, { @@ -126,7 +126,7 @@ const TOOLS: Tool[] = [ agentAudit: "no", transparentApi: "no", metrics: "yes", - mcpServer: "no", + mcpServer: "undocumented", }, }, ]; @@ -196,6 +196,10 @@ export function CompareMatrix() { No + + Not + documented + · Click a tool for the full breakdown diff --git a/website/src/components/compare-section.tsx b/website/src/components/compare-section.tsx index ce177cc6..c3e0f0f8 100644 --- a/website/src/components/compare-section.tsx +++ b/website/src/components/compare-section.tsx @@ -1,4 +1,4 @@ -import { ArrowRight, Check, Minus, X } from "lucide-react"; +import { ArrowRight, Check, CircleQuestionMark, Minus, X } from "lucide-react"; import Link from "next/link"; import { type CellValue, ComparisonCellIcon } from "@/components/comparison-cell-icon"; import { SectionHeading } from "@/components/section-heading"; @@ -59,10 +59,10 @@ const featureRows: FeatureRow[] = [ { label: "Read-only MCP server", portwing: "yes", - portainer: "no", - komodo: "no", - arcane: "no", - hawser: "no", + portainer: "undocumented", + komodo: "undocumented", + arcane: "undocumented", + hawser: "undocumented", }, ]; @@ -162,9 +162,13 @@ export function CompareSection() { Partial - + No + + Not + documented + diff --git a/website/src/components/comparison-cell-icon.tsx b/website/src/components/comparison-cell-icon.tsx index 5375f144..e62fd26f 100644 --- a/website/src/components/comparison-cell-icon.tsx +++ b/website/src/components/comparison-cell-icon.tsx @@ -1,6 +1,6 @@ -import { Check, Minus, X } from "lucide-react"; +import { Check, CircleQuestionMark, Minus, X } from "lucide-react"; -export type CellValue = "yes" | "partial" | "no"; +export type CellValue = "yes" | "partial" | "no" | "undocumented"; export function ComparisonCellIcon({ value }: { value: CellValue }) { if (value === "yes") { @@ -19,6 +19,17 @@ export function ComparisonCellIcon({ value }: { value: CellValue }) { ); } + if (value === "undocumented") { + return ( + <> +