A local web console for Apple's container runtime — the management UI Apple didn't ship, plus the reliability and orchestration layers container doesn't have.
Porthole is a single, self-contained binary that gives you a live topology of your
containers, full lifecycle control, restart policies and health checks, streaming
logs, an in-browser terminal, compose-style stacks, and one-click disk reclaim —
all from a web console that runs entirely on your Mac. It shells out to the
container CLI you already have and never phones home.
Apple's container is a clean, native container runtime for Apple silicon — but
it's CLI-only. There's no dashboard to see what's running, no UI to keep a
container alive across crashes, no place to watch logs or open a shell without
remembering the right flags. Porthole is that missing console. It doesn't replace
the runtime; it sits on top of the CLI and turns it into something you can see
and operate.
It also adds two things the runtime itself doesn't have: a supervision layer (restart policies + health checks, so a container can be kept alive) and a stacks layer (bring up a multi-container app from a compose file).
- macOS 26 (Tahoe) or later
- Apple silicon (M1 or newer)
container1.0 or later installed, with the system started:container system start
Verified against
container1.0.0 (commitee848e3).containeris a young, fast-moving runtime; Porthole pins to the documented 1.x CLI contract and parses--format jsonoutput. If you're on a different build and something looks off, please open an issue with yourcontainer --version.
brew tap dariusvorster/porthole brew trust dariusvorster/porthole brew install porthole brew services start porthole
Then open http://127.0.0.1:9191 .
brew services runs Porthole as a per-user launchd agent that starts on login.
To run it by hand instead, just launch the binary:
portholed
You'll need Go 1.2x and Node 20+.
git clone https://github.com/dariusvorster/porthole cd porthole make build # builds the web UI and embeds it into a single binary ./bin/portholed
The result is one cgo-free arm64 binary with the entire web console embedded —
nothing else to deploy.
- Create — run a container from the UI with the full run surface: image, published ports, environment, volumes (including host-path binds), labels, network, resource limits, and command. Set a restart policy at create and it's supervised from birth.
- See — a live topology view (host → network → container) as the home screen, with a dense list view and a detailed inspector. Everything updates in real time over a server-sent-events stream.
- Control — start, stop, restart, kill, and delete, with optimistic UI, destructive confirms, and a fully typed error model (no raw CLI errors leaking to you).
- Supervise — restart policies (
always,unless-stopped) plus HTTP/TCP health checks, with crash-loop backoff and a give-up ceiling. This is the reliability layercontaineritself lacks: stop a supervised container out of band and Porthole brings it back; stop it from Porthole and it stays down. - Stream logs — per-container live logs, ring-buffered, with clean teardown when the container stops.
- Exec — a real interactive terminal in your browser (xterm.js over a host PTY), with resize support.
- Orchestrate — import a compose file (a documented subset) and bring a multi-service
app up or down. Per-stack network isolation, supervision wired from
restart:labels, topology grouping, and service discovery (stack services resolve each other by name, injected and kept current across restarts). Compose-file drift is detected and shown as a diff; you can apply a recreate explicitly — it stops, replaces, and rolls back to the previous version if the new one fails to start, preserving named-volume data. - Reclaim disk — manage images, volumes, and networks with a preview-then-apply prune.
Porthole surfaces the anonymous-volume leak (orphaned volumes the runtime never
cleans up on
--rm) that's otherwise invisible, and reclaims it in one click. - Authenticate — log in to private registries (Docker Hub and others) from the
UI so private images pull. Your token is piped straight to
containerand never stored by Porthole; the runtime owns the credential.
Inspector with logs, exec, and supervision badges
Porthole doesn't store anything itself — it uses container's own image store.
- Pulling: when you run a container,
containerpulls the image if it's not already local. A bare name likenginxresolves to Docker Hub (docker.io/library/nginx:latest); you can also give a fully-qualified reference (ghcr.io/owner/image:tag). Porthole streams the pull progress in the create form, and offers a standalone Pull action in the Resources view. - Private registries: log in from Settings → Registry (a failed pull also
nudges you there). Porthole pipes the token straight to
container registry loginover stdin and never stores it —containerowns the credential.- One-time keychain authorization: your first private pull may appear to
stall right at the start.
container's image helper reads your saved credential from the macOS keychain, and macOS raises a permission prompt the backgroundportholedcan't display — so the pull waits. Run the pull once in Terminal and click Always Allow:The grant is then stored on the keychain item (keyed to the helper, not the caller), so every later pull — including Porthole's — reads it silently. Porthole detects this stall and shows you this exact command instead of hanging.container image pull <your-private-image>
- One-time keychain authorization: your first private pull may appear to
stall right at the start.
- On disk: images, volumes, snapshots, and all runtime state live under
~/Library/Application Support/com.apple.container/(Porthole's own state — just supervision policies and stack definitions — is separate; see Configuration). The Resources view shows what's reclaimable and reclaims it with a preview-then-apply prune, including the orphaned anonymous volumes the runtime otherwise leaves behind.
Porthole is localhost-only by design. This is deliberate, and it matters more than it might look:
- It binds to
127.0.0.1and refuses to bind to any non-loopback address without authentication. - Every state-changing request — and the exec WebSocket upgrade — passes an Origin/Host allow-list that defeats DNS-rebinding and CSRF attacks from a browser, even though there's no login.
- It runs as your user, not root.
- No data ever leaves your machine.
The exec terminal is the reason this is non-negotiable: a browser-reachable interactive shell into a container, exposed on the network without auth, would be a remote root shell. The trust model for v0.1 is simply "anyone who can already open a terminal on this Mac." That's why remote access is a v2 feature gated on authentication, not a flag you can flip — see the roadmap.
Most people need none of this — the defaults work. But:
| What | How | Default |
|---|---|---|
| Listen address | -addr flag |
127.0.0.1:9191 |
Path to the container CLI |
-container-bin flag |
container (on PATH) |
| Max supervised restart attempts before giving up | PORTHOLE_MAX_RESTARTS env |
a sane built-in ceiling |
| Persistent state (policies, stacks, restart counts) | SQLite at ~/Library/Application Support/porthole/porthole.db |
created on first run |
| Version | portholed -version |
— |
The data directory must be writable by the user the agent runs as (it is, by default, since Porthole runs as you). Supervision policies, stack definitions, and cumulative restart counts persist there across restarts.
Most of the original v2 roadmap has shipped — service discovery, drift remediation, health checks at create, richer create flags, registry login, and selectable rows are all in. What's left is deliberately deferred, and the right next steps are best decided by real use, so feedback on ordering is genuinely welcome:
- Remote access + authentication — reach Porthole from another machine (e.g. over Tailscale), gated on auth because of the exec shell surface. The one large remaining item; held until there's real demand, since it changes the security model.
- Health-gated dependency ordering — start stack services in dependency order, waiting on health (distinct from the health checks Porthole already supports).
on-failurerestart policy — currently impossible: the runtime doesn't expose a container's exit code, so "restart only on failure" can't be implemented faithfully. Tracked upstream.
Porthole is a Go daemon (portholed) that wraps the container CLI — it shells out,
parses the JSON output, and exposes a small REST + SSE API on 127.0.0.1:9191. The web
console (React + Vite + TypeScript + Tailwind) is compiled and embedded into the binary
at build time via go:embed, so the whole thing ships as one cgo-free Mach-O arm64
file with no runtime dependencies beyond container itself. Persistent state lives in a
pure-Go SQLite database; there's no external datastore.
The runtime is reached through an Engine interface (CLI-backed today), which keeps a
future native path additive.
make build # build UI + embed + compile go test -race ./... # backend tests npm --prefix web run test # frontend tests npm --prefix web run build # build the UI alone
Issues and pull requests welcome. If you're reporting a bug, please include your macOS
version, container --version, and your Apple-silicon model — a fair number of reports
come down to environment mismatches.
MIT.