Skip to content

Getting Started

Installation

Install the Localport agent on macOS, Linux, or Windows from Homebrew, apt, dnf, apk, a container image, the install script, or source.

Summary

The Localport agent is a single binary with no runtime and no dependencies. Install it with one command, from the signed package repositories, as a container image, or build it yourself from source. Every release is signed and carries build provenance. It runs on macOS, Linux, and Windows, on Intel and ARM.

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 | sh

Everywhere else the script installs the binary. It checks the download against the release's checksums.txt, and checksums.txt against the release signature when the machine has ssh-keygen from OpenSSH 8.1 or later, which macOS, Windows 10 and later, and most Linux systems do. A mismatched checksum or a bad signature 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.

Apart from the repository entry and its key on the Linux distributions above, the script changes nothing else: no shell profile, no background service, no telemetry. Both scripts 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. A localport package from the Localport repository is upgraded through its package manager.

Any other 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.

A binary the script installed before the package repository existed stays a binary install at its own path. To move a Debian, RHEL-family or Alpine machine onto the repository, delete that binary and run the script again.

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.

VariableEffect
LOCALPORT_VERSIONInstall a specific release tag, such as v0.8.0, instead of the latest stable
LOCALPORT_INSTALL_DIRInstall the binary to a directory you choose, on macOS, Linux and Windows. Skips the package repository
LOCALPORT_FORCEReinstall 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

Set LOCALPORT_VERSION to a release tag. On a machine you rebuild often, an unexpected version change is worse than an old version.

Install from the package repository#

pkg.localport.io serves signed apt, dnf and apk repositories for x86_64 and ARM64, plus 32-bit ARM for Debian-family systems and Alpine. The install script sets it up for you. Set it up by hand when you provision machines with Ansible, cloud-init or an image build, where a pinned, declarative source beats running a script.

Each repository carries the last 10 stable releases. Prereleases are published on GitHub only.

Covers Debian, Ubuntu, Raspberry Pi OS and their derivatives. The key is scoped to this repository with signed-by, so it vouches for nothing else on the machine.

sudo install -d -m 0755 /usr/share/keyrings
curl -fsSL https://pkg.localport.io/localport-archive-keyring.gpg \
  | sudo tee /usr/share/keyrings/localport-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/localport-archive-keyring.gpg] https://pkg.localport.io/deb stable main" \
  | sudo tee /etc/apt/sources.list.d/localport.list
sudo apt-get update
sudo apt-get install localport localport-archive-keyring

localport-archive-keyring owns the key file from then on, so a future key change arrives as an ordinary package upgrade. Pin a version with sudo apt-get install localport=0.8.0-1.

Upgrade with apt upgrade localport, dnf upgrade localport or apk upgrade localport, alongside the rest of the system.

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/localport

Check the release signature#

A checksum proves the file matches checksums.txt. The signature proves checksums.txt is the one Localport released. Each release carries three independent proofs, and any one of them is enough.

The release key. A maintainer signs checksums.txt with a key held in hardware. Download checksums.txt.sig and allowed_signers next to checksums.txt, then check it with OpenSSH 8.1 or later. The command is the same on macOS, Linux and Windows:

ssh-keygen -Y verify -f allowed_signers -I release@localport.io \
  -n localport-release -s checksums.txt.sig < checksums.txt

Build provenance. Every file has a SLSA provenance attestation naming the workflow, commit and tag that built it. Check one with the GitHub CLI:

gh attestation verify localport-linux-amd64 --repo localport/agent \
  --signer-workflow localport/agent/.github/workflows/release.yml

Sigstore. checksums.txt.sigstore.json is a keyless signature by the release workflow, checked with cosign 3.1.3 or later:

cosign verify-blob checksums.txt --bundle checksums.txt.sigstore.json \
  --certificate-identity-regexp '^https://github\.com/localport/agent/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Each release also publishes an SPDX software bill of materials, localport.spdx.json, listing the Go modules the agent is built from.

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.27 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.

Run the agent in a container#

The official image is ghcr.io/localport/agent, for linux/amd64, linux/arm64 and linux/arm/v7. It holds the release binary, CA certificates and nothing else, and runs as an unprivileged user (UID 65532). Tags are the release version, such as 0.8.0, and latest for the newest stable release.

docker run --rm -e LOCALPORT_TOKEN=tok_... ghcr.io/localport/agent:0.8.0 \
  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.

Credentials from localport setup or localport login live in /home/nonroot/.localport. Mount a volume there to keep them across container restarts:

docker run -d --restart unless-stopped \
  -v localport:/home/nonroot/.localport \
  ghcr.io/localport/agent:0.8.0 connect --config /home/nonroot/.localport/localport.yaml

The image is signed by the release workflow. Check the signature with cosign 3.1.3 or later before you deploy it:

cosign verify ghcr.io/localport/agent:0.8.0 \
  --certificate-identity-regexp '^https://github\.com/localport/agent/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

To add the agent to an image of your own, copy the binary from the official image by digest, which Docker checks on pull:

COPY --from=ghcr.io/localport/agent:0.8.0@sha256:<digest> /usr/bin/localport /usr/local/bin/localport

On an Alpine-based image, install the package from the repository instead, so apk verifies it.

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/bin for a package, /usr/local/bin for the script 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, and a package from the repository upgrades with the rest of the system through apt, dnf or apk. 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, sudo apt-get remove localport, sudo dnf remove localport or apk del localport for a package, otherwise sudo rm /usr/local/bin/localport, or delete the folder under %LOCALAPPDATA%\Programs on Windows. To remove the repository as well, delete /etc/apt/sources.list.d/localport.list, /etc/yum.repos.d/localport.repo or the pkg.localport.io line in /etc/apk/repositories.

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

The certificate stays valid until you revoke it in the dashboard. If the machine is being decommissioned or has been lost, revoking is the step that matters. See Certificates.

Frequently asked questions#

Which operating systems and processors does the agent support?

The agent runs on macOS 13 Ventura or later, Windows 10 or Windows Server 2016 or later, and Linux with kernel 3.2 or later. Every release publishes six builds: macOS on Apple Silicon and Intel, Linux on x86_64, ARM64 and 32-bit ARM (ARMv6 and ARMv7), and Windows on x86_64. 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 set up the package repository or to write to /usr/local/bin, and falls back to ~/.local/bin in your home directory when sudo 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 file, signed by the release key. The install script checks both and aborts on a mismatch, and packages from the repository are checked by apt, dnf or apk. 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, then check the release signature.

Does the agent install a background service or a system extension?

No. Installing puts one binary on disk, plus the repository entry and its key when installed as a package. 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. The linux-arm64 build covers a Raspberry Pi 3 or newer on a 64-bit operating system, the default on current Raspberry Pi OS. The linux-arm build covers every Raspberry Pi on a 32-bit operating system, including the Pi Zero and Pi 1. On Raspberry Pi OS the install script sets up the package repository, so the right build is chosen for you.

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, brew upgrade localport if you installed with Homebrew, or upgrade the localport package with apt, dnf or apk. 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 macOS, Linux and Windows, or pin the package version as shown under Install from the package repository. 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.27, and is the only Localport component that runs inside your network. Its wire protocol is documented in the same repository.

  • Quickstart. 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.