PublicLocal

Documentation

Everything you need to share localhost with PublicLocal, from the first command to running your own server. Updated 30 October 2026 · v1.0

Install

PublicLocal is one static binary for Linux, macOS, Windows and FreeBSD (amd64 and arm64).

# Linux / macOS
curl -fsSL https://publiclocal.app/install.sh | sh

# Windows (PowerShell)
irm https://publiclocal.app/install.ps1 | iex

# Docker
docker run --rm --network host ghcr.io/sevenoways/publiclocal http 3000

# Go
go install github.com/sevenoways/publiclocal/cmd/publiclocal@latest

Both installers download the build for your system from GitHub Releases and verify its SHA-256 checksum before installing. Run the same command again to update.

Quick start

Start your app, then share its port:

$ publiclocal http 3000
Forwarding https://k7xq2m.publiclocal.app -> 127.0.0.1:3000
Link expires in 24 hours; run `publiclocal login` for 7-day links

Anyone with the link reaches your app over HTTPS. Press Ctrl+C to stop. If you run the command again later, you get the same link back until it expires; add --new for a different one. You can also forward to another host on your network: publiclocal http 192.168.1.20:8080.

Browsers see a short "temporary developer link" notice the first time they open a link on the free server. Webhooks, API calls and curl go straight through, and you can skip the notice in any client by sending the header PublicLocal-Skip-Warning: 1.

Watch requests, replay webhooks

While a tunnel runs, every request is printed as it is served (--quiet turns it off), and the inspector at localhost:4040 shows each one with headers and bodies. Select a request and click Replay against localhost to send it to your app again, which is how you fix a webhook handler without triggering the webhook over and over. --inspect "" turns the inspector off; --inspect 127.0.0.1:5050 moves it.

14:02:11  POST   /webhooks/stripe                          200    12 ms  54.187.174.169
14:02:13  GET    /api/orders/42                            404     3 ms  203.0.113.7
^C
Served 2 requests from 2 visitors · 4 KB · top: /webhooks/stripe (1), /api/orders/42 (1)
$ publiclocal http --password s3cret 3000     # visitors must enter the password (any user name)
$ publiclocal http --max-visits 1 3000         # only the first person who opens it can use it
$ publiclocal http --expires 30m 3000          # gone after half an hour
$ publiclocal http --note "Staging build for Anna" 3000
$ publiclocal http --qr 3000                   # QR code in the terminal, for your phone
$ publiclocal http --cors 3000                 # permissive CORS headers for browser apps
$ publiclocal http 3000 8080                   # two links in one go

The password is checked by the server before anything reaches your app, and your app never sees the password header. The note appears on the first page visitors see, together with your GitHub name if you are signed in. --max-visits counts visitor networks: the first N people who open the link keep access, everyone else gets "visitor limit reached".

Share a folder or a file

$ publiclocal share ./build          # a folder, with a listing
$ publiclocal share report.pdf       # a single file, served at /

No web server needed: PublicLocal serves the files itself. Folders with an index.html are served as a site; others get a tidy listing. Dotfiles are hidden. All the link flags above apply, so publiclocal share --password s3cret --max-visits 1 report.pdf hands one file to one person.

Link status, badge and stats

Every link answers /.publiclocal/status with JSON (state, expires, requests, visitors, bytes) and /.publiclocal/badge.svg with a small preview: online / offline / expired badge for a README or pull request. When a link is offline or has expired, visitors see a page saying so, with when it was live and how much traffic it served.

Sign in for 7-day links

$ publiclocal login            # opens GitHub sign-in; type the code shown in your terminal
$ publiclocal http 3000        # links now last 7 days
$ publiclocal extend k7xq2m    # another 7 days, as often as you like
$ publiclocal links            # your links and when they expire
$ publiclocal logout

Sign-in reads only your public GitHub profile and is used to tie links to you. GitHub accounts younger than 30 days cannot sign in, which keeps throwaway accounts out. To take a link back by name after a restart, use publiclocal http --link k7xq2m 3000.

Command reference

CommandWhat it does
publiclocal http <port> [port...]Public HTTPS links for local HTTP servers. Flags: --password, --max-visits, --expires, --note, --cors, --qr, --inspect, --quiet, --new, --link NAME, --subdomain NAME (own server), --server HOST, --token.
publiclocal share <path>Serve a folder or file from this computer and share it. Same flags as http.
publiclocal tcp <port>Public TCP port for SSH, databases or game servers. Self-hosted servers only. --remote-port N asks for a fixed port.
publiclocal login / logoutSign in with GitHub for 7-day links; sign out and revoke the token.
publiclocal linksList your links, whether they are online, and when they expire.
publiclocal extend <link>Push a link's expiry to 7 days from now (signed in).
publiclocal mcpRun the MCP server for AI assistants. Flags: --allow-ports 3000,5173, --max-tunnels.
publiclocal serverRun your own tunnel server. See self-hosting.
publiclocal adminOn your own server: list, kill, ban, suspend, flags, scan.
publiclocal versionPrint the version.

Environment variables: PUBLICLOCAL_SERVER, PUBLICLOCAL_TOKEN (own server), PUBLICLOCAL_ACCOUNT (login token). Without --server the client uses the free server at publiclocal.app.

Self-hosting on your own domain

Your own server gives you addresses on your domain, names you choose, no expiry, TCP tunnels and your own limits. Any small VPS with a public IP works; bandwidth matters more than CPU.

1. DNS

Point a name and its wildcard at the server (the wildcard is what gives each tunnel its own subdomain):

tunnel.example.com.    A  203.0.113.10
*.tunnel.example.com.  A  203.0.113.10

2. Server

Open ports 80, 443, 7000 and the TCP range (default 10000–20000), then:

curl -fsSL https://publiclocal.app/install.sh | sh
publiclocal server --domain tunnel.example.com --tokens "$(openssl rand -hex 24)" --acme-email you@example.com

Certificates are issued by Let's Encrypt per subdomain on first use. Busy servers can use one wildcard certificate instead with --cloudflare-token (a Cloudflare API token that may edit the zone's DNS). A systemd unit and Docker image are in the public-instance guide.

3. Clients

export PUBLICLOCAL_SERVER=tunnel.example.com
export PUBLICLOCAL_TOKEN=the-token-from-above

publiclocal http --subdomain myapp 8080   # https://myapp.tunnel.example.com
publiclocal tcp 22                        # tcp://tunnel.example.com:1xxxx
publiclocal tcp --remote-port 15432 5432

Server flags

FlagDefaultMeaning
--domainBase domain; tunnels are served at name.domain. Required.
--tokensComma-separated tokens clients must present. Empty allows anyone.
--control:7000Port clients connect to.
--http, --https:80, :443Set --https "" to run plain HTTP behind your own proxy (clients then need --tls=false).
--tcp-ports10000-20000Port range for TCP tunnels.
--cloudflare-tokenOne wildcard certificate via Cloudflare DNS instead of a certificate per name.
--state-fileKeep links and accounts across restarts.
--publicoffSafeguards for an open instance: random names only, no TCP, warning page, limits, misuse checks. Needs --abuse-contact.
--max-tunnel-agenever (public: 24h)Link lifetime.
--github-client-id, --github-client-secretEnable publiclocal login with your own GitHub OAuth app.
--admin, --admin-token127.0.0.1:7001Local admin API for publiclocal admin.

The full list is in publiclocal server -h, and the guide for running an open public instance (abuse handling, Public Suffix List, CAA records) is in the repository.

AI assistants (MCP)

PublicLocal ships an MCP server with three tools: share_local_port, list_tunnels and stop_tunnel. It runs on your machine, started by the assistant, because the tunnel has to begin where your app runs. Tunnels close when the assistant session ends.

# Claude Code
claude mcp add publiclocal -- publiclocal mcp

# OpenAI Codex CLI
codex mcp add publiclocal -- publiclocal mcp

# Claude Desktop (claude_desktop_config.json), Cursor (.cursor/mcp.json), Gemini CLI (~/.gemini/settings.json)
{ "mcpServers": { "publiclocal": { "command": "publiclocal", "args": ["mcp"] } } }

# VS Code (.vscode/mcp.json)
{ "servers": { "publiclocal": { "type": "stdio", "command": "publiclocal", "args": ["mcp"] } } }

Add "--allow-ports", "3000,5173" to args so the assistant can only ever share the ports you list. ChatGPT's apps connect only to remote MCP servers and cannot start programs on your computer, so use the Codex CLI for OpenAI models. Full details: MCP guide.

Free server limits

Troubleshooting

"local service unavailable" when opening the link: nothing is listening on the port you shared, or your app only listens on a specific interface. Check with curl localhost:3000.

Your app redirects to the wrong host: the original Host header is passed through, and X-Forwarded-Proto: https is set. Frameworks that check allowed hosts (Django ALLOWED_HOSTS, Rails config.hosts, Vite server.allowedHosts) need the tunnel host added.

Webhooks hit the warning page: they shouldn't; the notice is only for browsers. If a service sends a browser-like user agent, add the header PublicLocal-Skip-Warning: 1 to its request if you can, or self-host without --public.

"This app can't run on your PC" on Windows: you downloaded the ARM64 build. Use the installer command, which picks the right one.

Anything else: open an issue.