Automatic port forwarding and browser URL opening between devcontainers and the host machine for CLI users.
This project was initially built as an exploratory collaboration between a @bradleybeddoes and Claude Code agent teams.
Considerable planning and design was undertaken by @bradleybeddoes including technology choice. The codebase, tests, documentation, and E2E validation scripts were created by an initial team of 17 agents and then refined over another few hours of pair programming, the team did exceptionally well but the application didn't function straight "out of the box".
Note
This project is in early development. Bugs are expected. It has only been tested with a macOS host so far — Linux host support is planned but unverified.
The devcontainer CLI lacks two features that VS Code provides transparently:
- Port forwarding — When a process inside a devcontainer listens on a port (e.g., a dev server on
:3000), VS Code automatically makes it accessible on the host. The devcontainer CLI does not. - Browser opening — When a container process tries to open a URL (e.g., an OAuth callback), VS Code opens it in the host browser. The devcontainer CLI cannot.
This breaks workflows like OAuth flows (which bind a random port, open a browser, and expect a callback on localhost), dev servers, and any tool that needs host-side access.
dbr fixes both, with zero changes to shared devcontainer.json files.
VS Code users are not impacted. The
dbrbinary is inert unless explicitly started. It does not set global environment variables, start background processes, or interfere with VS Code's own port forwarding. Teams can safely include the devcontainer feature — it's like havingnviminstalled but unused.
┌───────────────────── Host Machine (macOS/Linux) ────────────────────┐
│ │
│ dbr host-daemon (long-lived, auto-started) │
│ ├─ Control: :19285 (JSON-lines protocol) │
│ ├─ Data: :19286 (reverse data connections) │
│ ├─ Accepts control connections from multiple containers │
│ ├─ Binds loopback:PORT for each forwarded port │
│ ├─ Bridges client connections ↔ reverse data connections │
│ └─ Opens URLs in host browser (open/xdg-open) │
│ │
└─────────────────────────────────────────────────────────────────────┘
▲ All connections initiated container → host
│ via host.docker.internal
┌─────────┴────────── Devcontainer (Linux) ───────────────────────────┐
│ │
│ dbr container-daemon │
│ ├─ Polls /proc/net/tcp every 1s for new listeners │
│ ├─ Sends Forward/Unforward to host via control channel │
│ ├─ Opens reverse data connections for proxied traffic │
│ └─ Reconnects automatically on connection loss │
│ │
│ BROWSER=dbr-open (set in personal dotfiles) │
│ │
└─────────────────────────────────────────────────────────────────────┘
All TCP connections flow container → host (reverse connection model). This is required because macOS Docker Desktop runs containers inside a Linux VM — the host cannot initiate connections into the container.
Control and data ports bind to an auto-detected address: 0.0.0.0 when Docker is running (so containers can reach the host), 127.0.0.1 otherwise. Override with --bind-addr or --no-docker-detect. Forwarded per-port listeners always bind to loopback only.
- Client connects to
host:8080 - Host daemon sends
ConnectRequestto container via control channel - Container daemon connects to
localhost:8080inside the container - Container daemon opens a reverse TCP connection to
host:19286(data channel) - Host daemon bridges the client and data connections bidirectionally
- When either side closes, both connections tear down
curl -fsSL https://github.com/bradleybeddoes/devcontainer-bridge/releases/latest/download/install.sh | bashOr build from source:
cargo install --path .Add the devcontainer feature to your project's devcontainer.json:
This installs the dbr binary and creates the dbr-open hardlink. The container daemon starts automatically via the feature's entrypoint — no manual setup required.
Add to your ~/.zshrc or ~/.bashrc inside the container (via your personal dotfiles):
export BROWSER=dbr-opendbr ensure # starts host daemon if not already runningThe container daemon is already running (auto-started by the devcontainer feature on boot).
# On the host, check active forwards
dbr statusContainer Port Host Port Process Since
myapp_dev 8080 8080 node 2m ago
myapp_dev 39821 39821 mcp-auth 5s ago
dbr host-daemon Start the host-side daemon
dbr container-daemon Start the container-side daemon
dbr ensure Start host daemon if not already running
dbr status Show active port forwards across all containers
dbr forward PORT Manually forward a port
dbr unforward PORT Manually remove a port forward
dbr open URL Open a URL in the host browser
dbr host-daemon [--bind-addr ADDR] [--no-docker-detect]
[--control-port PORT] [--data-port PORT]
[--browser-cmd COMMAND]
[--log-level LEVEL] [--log-format text|json]
[--log-file PATH] [--exit-on-idle]By default, the host daemon auto-detects whether to bind to 0.0.0.0 (Docker running) or 127.0.0.1 (no Docker). Use --bind-addr to set an explicit address, or --no-docker-detect to force loopback. Use --browser-cmd to override the default browser command (e.g., /usr/bin/true for headless testing). The daemon stays running after the last container disconnects; pass --exit-on-idle to exit instead.
dbr container-daemon [--host-addr ADDR] [--scan-interval MS]
[--exclude-ports 22,5432]
[--log-level LEVEL] [--log-format text|json]
[--log-file PATH]The container daemon resolves the host address in this order:
--host-addrflagDCBRIDGE_HOSTenvironment variablehost.docker.internalDNS- Docker gateway IP from the container's default route
Set BROWSER=dbr-open in your container shell profile. Most tools (Node.js open, Python webbrowser, Rust open crate) respect this variable. For tools that call xdg-open directly:
ln -sf /usr/local/bin/dbr-open /usr/local/bin/xdg-openURLs are rewritten automatically — if container port 3000 is forwarded to host port 3001, http://localhost:3000/callback becomes http://localhost:3001/callback.
Add dbr ensure to your container startup workflow so the host daemon is
running before the container boots:
# Start host daemon (idempotent), then launch the devcontainer
dbr ensure
devcontainer up --workspace-folder "$folder"The container daemon starts automatically via the devcontainer feature — no manual launch needed.
One host daemon serves all running devcontainers. When multiple containers forward the same port, conflicts are resolved automatically:
- First container gets
host_port == container_port(8080 → 8080) - Subsequent containers get the next available port (8080 → 8081)
dbr statusshows the full mapping
- Two-tier binding model — Forwarded per-port listeners bind to loopback only (
127.0.0.1/[::1]), never0.0.0.0. Control and data ports use auto-detected binding (0.0.0.0when Docker is detected,127.0.0.1otherwise), overridable via--bind-addror--no-docker-detect - Only
http://andhttps://URLs accepted for browser opening - No Docker socket access required
- No elevated privileges needed
- Rate limiting on browser opens and resource caps on connections, containers, and forwards
- All events logged with timestamps and container IDs
- Zero
unsafeRust code
See docs/security.md for the full threat model and security guarantees.
# Build
cargo build --release
# Run unit + integration tests
cargo test
# Run end-to-end tests (self-contained, no external devcontainers needed)
scripts/dev-test.sh
# Lint
cargo clippy -- -D warnings
cargo fmt --checkStatic Linux binaries (for use inside containers):
cargo build --release --target x86_64-unknown-linux-musl
cargo build --release --target aarch64-unknown-linux-musl- Architecture & Protocol — reverse connection model, protocol spec, data flow
- Security Model — threat model, security guarantees, audit guidance
- CLI Developer Guide — terminal workflow setup, troubleshooting
- Team Adoption Guide — adding to shared configs, VS Code compatibility FAQ
- Development Guide — building, testing, debugging, and iterating on
dbr
See LICENSE for details.
{ "features": { "ghcr.io/bradleybeddoes/devcontainer-bridge/dbr:latest": {} } }