Installation
Install the Localport agent on macOS, Linux, or Windows. One binary, no dependencies, checksum verified. Homebrew, install script, manual download, or source.
Summary
Install the agent Recommended#
Run the command for your operating system.
Install with Homebrew:
brew install localport/tap/localport
Or use the install script:
curl -fsSL https://localport.io/install.sh | shcurl -fsSL https://localport.io/install.sh | shIn PowerShell:
irm https://localport.io/install.ps1 | iex
On Windows a fresh install goes under %LOCALAPPDATA%\Programs\localport and adds that folder to your PATH. No administrator rights needed.
Open a new terminal afterwards
A mismatched checksum aborts the install. On macOS and Linux the binary lands in /usr/local/bin, falling back to ~/.local/bin when that is not writable, and sudo is used only when it is needed. The script changes nothing else: no shell profile, no background service, no telemetry. Both are short enough to read first, at install.sh and install.ps1.
The same command installs and upgrades. On a machine that already has the agent, the script replaces the binary at the path it already occupies, so you never end up with two copies. If that binary is already the release being installed, the script says so and downloads nothing.
An agent that a package manager put there is left alone. The script recognises Homebrew, Nix, Snap, apt, dnf, pacman and apk on macOS and Linux, and Scoop, Chocolatey and winget on Windows. It stops and prints that manager's own upgrade command. Overwriting files a package manager owns leaves it out of step, and its next upgrade puts its own build back.
After installing, the script checks which localport your shell resolves. If an older copy sits earlier on your PATH, it names that file so you can delete it.
An agent installed by an earlier version of the script is found and upgraded even when your PATH does not reach it. The one exception is an install you placed somewhere custom with LOCALPORT_INSTALL_DIR: set the same variable again to upgrade it, or the script treats the machine as a fresh install.
Three environment variables change what gets installed and where.
| Variable | Effect |
|---|---|
LOCALPORT_VERSION | Install a specific release tag instead of the latest stable |
LOCALPORT_INSTALL_DIR | Install to a directory you choose, on macOS, Linux and Windows |
LOCALPORT_FORCE | Reinstall even when the installed binary is already current |
LOCALPORT_INSTALL_DIR="$HOME/bin" curl -fsSL https://localport.io/install.sh | sh
Pin the version on servers and in CI images
LOCALPORT_VERSION to a release tag. On a machine you rebuild often, an unexpected version change is worse than an old version.Install manually from a release#
Every release publishes the binaries and a checksums.txt covering all of them.
Verify it against checksums.txt, then install it. macOS adds a quarantine flag to anything downloaded from a browser, which xattr clears:
shasum -a 256 --ignore-missing -c checksums.txt
chmod +x localport-darwin-*
xattr -d com.apple.quarantine localport-darwin-*
sudo mv localport-darwin-* /usr/local/bin/localportsha256sum --ignore-missing -c checksums.txt
chmod +x localport-linux-*
sudo mv localport-linux-* /usr/local/bin/localport
ARM64 covers a Raspberry Pi 3 or newer on a 64-bit operating system. That is the default on current Raspberry Pi OS. A 32-bit install needs to be reimaged or built from source.
Compare the hash against checksums.txt, then rename the file and clear the block Windows puts on downloads:
Get-FileHash localport-windows-amd64.exe -Algorithm SHA256
Rename-Item localport-windows-amd64.exe localport.exe
Unblock-File localport.exe
Move localport.exe into a folder on your PATH, or add its folder to the PATH environment variable.
Build it from source#
The agent is the only part of Localport that runs on your machine, and its source is public under the Apache 2.0 licence. Building it yourself takes two commands.
Requires Go 1.25 or newer.
git clone https://github.com/localport/agent.git
cd agent
make build
./bin/localport version
make build-all cross-compiles every published platform into bin/. Releases are built with -trimpath from the same targets, so a binary you build matches the one we publish in behaviour and origin.
Run the agent as a background service#
A tunnel is live only while the agent is running. On a laptop you leave it in a terminal. On a server, a device in the field, or a box at home, you want it started at boot and restarted if it stops.
The repository ships both service templates, already hardened.
Take deploy/localport.service from the repository.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin localport
sudo install -d -m 0700 -o localport -g localport /etc/localport
printf '%s' 'tok_...' | sudo tee /etc/localport/token >/dev/null
sudo chmod 0600 /etc/localport/token
sudo cp localport.service /etc/systemd/system/
sudo systemctl enable --now localport
Edit the ExecStart= line to match the service you are forwarding, then check it with systemctl status localport and journalctl -u localport -f.
The token is never put on the command line. systemctl show, ps, and the unit file itself are readable by every local account, so a token in ExecStart= is a token every user on that machine has. The unit uses systemd's LoadCredential=, which places the token at a file readable only by the service user, and points LOCALPORT_TOKEN_FILE at it. The unit also runs unprivileged with ProtectSystem=strict, NoNewPrivileges=true, and a restricted set of address families.
Take deploy/io.localport.agent.plist from the repository.
Create the service account the daemon runs as. The UID is the next free one below 500, where macOS keeps hidden system accounts:
ID=$(( $(dscl . -list /Users UniqueID | awk '$2 < 500 {print $2}' | sort -n | tail -1) + 1 ))
sudo dscl . -create /Groups/_localport PrimaryGroupID "$ID"
sudo dscl . -create /Users/_localport UniqueID "$ID"
sudo dscl . -create /Users/_localport PrimaryGroupID "$ID"
sudo dscl . -create /Users/_localport RealName "Localport agent"
sudo dscl . -create /Users/_localport UserShell /usr/bin/false
sudo dscl . -create /Users/_localport NFSHomeDirectory /var/empty
sudo dscl . -create /Users/_localport IsHidden 1
Then install the daemon. Its state, its token and its log are owned by that account.
sudo install -d -m 0700 -o _localport -g _localport /Library/Application\ Support/localport
printf '%s' 'tok_...' | sudo tee /Library/Application\ Support/localport/token >/dev/null
sudo chown _localport:_localport /Library/Application\ Support/localport/token
sudo chmod 0600 /Library/Application\ Support/localport/token
sudo touch /var/log/localport.log
sudo chown _localport:_localport /var/log/localport.log
sudo cp io.localport.agent.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/io.localport.agent.plist
sudo chmod 0644 /Library/LaunchDaemons/io.localport.agent.plist
sudo launchctl load -w /Library/LaunchDaemons/io.localport.agent.plist
launchd has no equivalent of LoadCredential=, so the token goes in a file with mode 0600 that only this account reads, named by LOCALPORT_TOKEN_FILE. Keep it out of ProgramArguments, where ps shows it to every local account. launchd also has no sandboxing keys, so UserName is what bounds the daemon: it forwards a port above 1024 and needs nothing more. Logs go to /var/log/localport.log.
Windows has no service template in the repository. Use the Task Scheduler with a trigger of At startup, running localport.exe with your arguments, or wrap it with a service manager such as NSSM.
Set the token through the LOCALPORT_TOKEN or LOCALPORT_TOKEN_FILE environment variable on the task. A token on the command line appears in the task definition and in a process listing.
Run the agent in a container#
Localport does not publish a container image. The agent is a self-contained binary, so a short Dockerfile is all it needs. The ca-certificates package matters: the agent verifies the Localport server against the system trust store, and a bare image has no roots to verify against.
Download and check the binary first, using the commands in the section above, then copy the verified file into the image. A build step that fetches a binary and does not check it breaks the chain everything else here maintains.
FROM alpine:3
RUN apk add --no-cache ca-certificates
COPY localport-linux-amd64 /usr/local/bin/localport
RUN chmod +x /usr/local/bin/localport
ENTRYPOINT ["localport"]
Pin the base image to whatever digest your policy requires.
docker run --rm -e LOCALPORT_TOKEN=tok_... my-localport \
http 3000 --local http://host.docker.internal:3000 --region eu
Pass the token as an environment variable or a Docker secret. An argument shows up in docker inspect and in a process listing. A container reaching a service on the host needs host.docker.internal on macOS and Windows, or --network host on Linux.
Verify, upgrade, and remove#
localport version
That prints the version, the commit it was built from, and the build date. localport --version does the same. If your shell says the command was not found, the install directory is not on your PATH: /usr/local/bin on macOS and Linux, ~/.local/bin when the script fell back to it, and %LOCALAPPDATA%\Programs\localport on Windows.
Upgrading is the same command you installed with. Homebrew uses brew upgrade localport. The install script finds the binary your shell resolves, compares it against the release you asked for, and replaces it only if the two differ. Set LOCALPORT_FORCE=1 to reinstall a binary that is already current. Running tunnels keep going on the version they started with until you restart the agent.
Removing it is deleting the binary. brew uninstall localport for Homebrew, otherwise sudo rm /usr/local/bin/localport, or delete the folder under %LOCALAPPDATA%\Programs on Windows.
If this machine holds credentials from localport setup or localport login, clear them too: localport identity remove <selector> for one, or delete ~/.localport for all of them.
Deleting a credential does not revoke it
Frequently asked questions#
Which operating systems and processors does the agent support?
macOS on Apple Silicon and Intel, Linux on x86_64 and ARM64, and Windows on x86_64. Those five builds are published for every release. Other platforms that Go targets can be built from source.
Do I need administrator or root access to install it?
No. On Windows the script installs to your user profile and needs no elevation at all. On macOS and Linux it uses sudo only to write to /usr/local/bin, and falls back to ~/.local/bin in your home directory when that is not available. You can also set LOCALPORT_INSTALL_DIR to any directory you can write to.
How do I check the binary I downloaded has not been tampered with?
Every release publishes a checksums.txt listing the SHA-256 of each binary. The install script compares it automatically and aborts on a mismatch. To check a manual download, run sha256sum --ignore-missing -c checksums.txt on Linux, shasum -a 256 --ignore-missing -c checksums.txt on macOS, or Get-FileHash on Windows.
Does the agent install a background service or a system extension?
No. Installing puts one binary on disk and nothing else. There is no daemon, no launch item, no kernel extension, and no telemetry agent. It runs when you run it. Setting it up to start at boot is a separate, deliberate step covered above.
Can I install it without a package manager or an install script?
Yes. Download the binary for your platform from the releases page, verify its checksum, make it executable, and put it somewhere on your PATH. It has no dependencies to resolve, so nothing else is required.
Does it run on a Raspberry Pi or another single-board computer?
Yes, on a 64-bit operating system, using the linux-arm64 build. That covers a Raspberry Pi 3 or newer running current Raspberry Pi OS. A 32-bit image needs to be reimaged or the agent built from source for that target.
Where does the agent store its files?
Only credentials, and only if you create some. localport setup and localport login write to ~/.localport, with directories at mode 0700 and files at 0600. Set LOCALPORT_HOME to move that elsewhere. The service templates set it so a system account has somewhere to write. A tunnel that authenticates with a token alone writes nothing.
How do I upgrade to a new version?
Run the install script again, or brew upgrade localport if you installed with Homebrew. The script upgrades the binary where it already sits, and exits without downloading when that binary is already the version being installed. Nothing is migrated and no configuration is rewritten. A running agent keeps using the version it started with until you restart it.
Why did the install script refuse to upgrade my Homebrew copy?
Because Homebrew owns that file. Replacing it would point Homebrew's records at a build that is no longer there, and brew upgrade would put its own build back the next time it ran. Run brew upgrade localport to update it. If you want a copy the install script manages, set LOCALPORT_INSTALL_DIR to a directory you own and install there. The same applies to Nix, Snap, apt, dnf, pacman, apk, Scoop, Chocolatey and winget.
Can I pin a specific version instead of the latest?
Yes. Set LOCALPORT_VERSION to a release tag before running the install script, on both macOS and Linux and Windows. This is what to do on production machines and in CI images, where an unexpected version change is worse than an old version.
Can I audit what the agent does before running it on a work machine?
Yes, and this is the intended path for anyone who has to justify it. The agent's full source is on GitHub under Apache 2.0, builds with make build on Go 1.25, and is the only Localport component that runs inside your network. Its wire protocol is documented in the same repository.
What to read next#
- Quick Start. Create a tunnel and reach it from the internet.
- CLI. Every command, flag, and environment variable.
- Configuration. Run several tunnels from one YAML file.
- HTTP Tunnels. Web apps, APIs, and webhook endpoints.
- Remote Access. Require a client certificate to connect.