Run local apps behind the portable proxy, auto allocate ports, and expose friendly local hostnames.
Install latest release:
curl -fsSL https://raw.githubusercontent.com/BlankParticle/portable/main/install.sh | bashInstall a specific release:
curl -fsSL https://raw.githubusercontent.com/BlankParticle/portable/main/install.sh | bash -s -- -v 0.0.1Install the system service and trust the local root certificate:
portable service install
portable proxy trustStart the proxy:
portable run myapp -- pnpm devVisit https://port.local to view the Web UI.
On macOS, *.port.local hosts are supported without dnsmasq; portable advertises tracked *.port.local app hosts over mDNS while they are running:
portable run web -H web.port.local -- pnpm devUse @PORT when your app expects a CLI flag instead of environment variables:
portable run dev-server -- vite dev --port @PORTBy default, portable uses a local development hostname when available, such as <name>.port.local, and falls back to <name>.localhost elsewhere.
On Linux, portable uses systemd-resolved and a local DNS server on 127.0.0.1:5354 for *.port.local hosts.
The macOS mDNS responder and Linux DNS server both run alongside the portable proxy.
On Linux, portable configures systemd-resolved to route *.port.local to the local DNS server automatically while the proxy is running.
portable doctor reports whether systemd-resolved is active on Linux and whether *.port.local routing is currently configured.
While the proxy is running, https://port.local serves the built-in control UI for the portable proxy. If DNS is unavailable, the same UI is also served from http://localhost:2019.
Store extra UI hostnames in the portable state file if you want the same UI served on additional names, such as a Tailscale DNS hostname:
portable config ui-hosts add port.local.tailnet.ts.net
portable config ui-hosts add proxy.example.internal
portable config ui-hosts lsFor local UI development with Vite, run pnpm dev in ui; the dev server proxies /portable/* to http://localhost:2019, so browser requests stay same-origin on http://localhost:5173.
portable also sets PORT=<allocated port>, PORTABLE_APP=<name>, and PORTABLE_HOST=<https URL> for the child process.
portable requires the portable proxy to own Caddy.
Start the proxy:
portable proxy startThe proxy can load your existing base Caddyfile so portable can replace a standalone Caddy install while keeping your custom sites. It looks in this order and uses the first file that exists:
$PORTABLE_CADDYFILE/etc/Caddyfile~/.config/caddy/Caddyfile
The embedded build also includes the Cloudflare DNS module, so directives like acme_dns cloudflare ... work in the loaded Caddyfile.
If a .env file exists next to the selected Caddyfile, portable loads it before adapting the file, so you can move provider tokens and other secrets out of it.
Privileged start (bind 80/443 if available):
portable proxy start -pportable run myapp -- pnpm dev
portable ls
portable rm <name>
portable proxy status
portable proxy reload
portable proxy trust
portable proxy stop
portable dns status
portable dns revert
portable doctorAll commands support --json for scriptable output.
Examples:
portable proxy status --json
portable ls --json
portable --json run api -- uvicorn app:app --port @PORTFor a self-hosted public relay, point your wildcard DNS at a machine running portable proxy start, then enable public tunnel serving there:
portable proxy start -p
portable tunnel server enable
portable tunnel server suffixes add public.comPair your local machine with that relay over SSH:
portable tunnel pair portable@relay.public.com
portable tunnel statusExpose an app publicly while keeping the local dev hostname:
portable run web --tunnel web.public.com -- pnpm devportable registers the public host on the relay, opens an SSH reverse tunnel back to the local app port, and removes the route when the app exits.
portable proxy trust fetches the local CA root from the Caddy admin API and installs trust using the same truststore approach used by Caddy.
For Node.js clients (fetch, undici, axios over HTTPS), you may also need to enable CA trust in Node:
- Newer Node versions: set
NODE_USE_SYSTEM_CA=1so Node uses system trust. - Older Node versions (or as a fallback): set
NODE_EXTRA_CA_CERTSto Caddy's local root cert path.
The Caddy local root cert path is resolved from the Caddy data directory in this order:
$PORTABLE_CADDY_DATA_DIR(if set)- else
$CADDY_DATA_DIR(if set) - else the platform default Caddy data directory (macOS:
~/Library/Application Support/Caddy, Linux:~/.local/share/caddy)
Append pki/authorities/local/root.crt to the selected data directory.
Example:
# Newer Node
export NODE_USE_SYSTEM_CA=1
# Older Node / fallback (macOS)
export NODE_EXTRA_CA_CERTS="$HOME/Library/Application Support/Caddy/pki/authorities/local/root.crt"
# Older Node / fallback (Linux)
export NODE_EXTRA_CA_CERTS="$HOME/.local/share/caddy/pki/authorities/local/root.crt"State is stored in:
$XDG_STATE_HOME/portable- fallback:
~/.local/state/portable
When the proxy is started with portable proxy start -p, portable keeps using the invoking user's state directory rather than a root-owned runtime directory.
Files:
state.jsonstate.lockdaemon.piddaemon.log
Use portable service to install and manage a system service on the current machine:
portable service install
portable service status
portable service restart
portable service stop
portable service uninstallFor supervised installs, set PORTABLE_RUNTIME_DIR and PORTABLE_CADDY_DATA_DIR explicitly so the proxy keeps using the intended user's state and Caddy storage directories.
On Linux, you can run portable as a normal user and grant only CAP_NET_BIND_SERVICE if you want it to bind 80/443 without running the entire service as root.
Build and install from local source:
make installSource code lives in cmd/portable.
Common root-level tasks:
make install
make openapi
make build
make test
make dev-ui
make fmtIf you prefer, ./install-dev.sh still works directly.