# Portr: a self-hosted SSH tunnel server for exposing local HTTP, TCP and WebSocket services

> Portr is a Go-based tunnel solution that uses SSH remote port forwarding to publish local development servers on public URLs, with a local inspector and CLI request logs. It is built for small teams, not production traffic.

**amalshaji/portr** — Expose local http, tcp or websocket connections to the public internet

- Repository: https://github.com/amalshaji/portr
- Website: https://portr.dev
- Stars: 3,194 · Forks: 115
- Language: Go
- License: AGPL-3.0
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/amalshaji-portr

## What problem Portr solves, and for whom

Portr publishes a service running on your laptop to the public internet. The README describes it as a tunnel solution that exposes local http, tcp or websocket connections, built on SSH remote port forwarding. The stated audience is small teams that need a public URL for a development server. The same README states plainly that it is not recommended for use alongside production servers, which is the single most useful sentence in the project's documentation.

The practical gap it fills is the one between a shared staging environment and a laptop. A webhook provider needs a reachable HTTPS endpoint. A colleague needs to hit your API without you building a deploy pipeline for a branch that will be discarded. A WebSocket client needs a live session to debug against. Portr gives each of those a URL and, unlike a bare SSH reverse tunnel, adds a local inspector and persisted request logs so you can see what arrived after the fact.

## SSH remote port forwarding with an inspector on top

The tunnelling mechanism is not a custom protocol. Portr relies on SSH remote port forwarding, and the go.mod file lists github.com/gliderlabs/ssh, which is the SSH server library the project uses. The client opens a connection to the Portr server, the server allocates a public hostname, and incoming requests are forwarded down that connection to your local port.

The parts layered on top are what distinguish it. Starting an HTTP tunnel does three things according to the README: it creates a public HTTPS URL, it starts a local inspector at http://localhost:7777, and it persists HTTP request logs locally so they can be queried from the CLI. The inspector supports inspecting requests and responses, replaying stored requests, examining headers and payloads, and monitoring upgraded WebSocket sessions and captured frames. The stored data lives in ~/.portr/db.sqlite on the client side, and the CLI reads from it.

The server side is a Go binary, portrd, built from ./cmd/portrd, with an admin dashboard for team, user and connection management. Persistence is configurable: the repository ships migrations, uses gorm with both SQLite and Postgres drivers, and the Docker image bundles litestream, which suggests SQLite replication is a supported deployment shape. The compose file exposes port 2222 for SSH, 8000 for the admin API and 8001 alongside it.

## Installing the client and running your first tunnel

The README does not inline installation commands for the client; it points to the client installation guide at portr.dev/docs/client/installation and to a server setup guide at portr.dev/docs/server. The repository does contain an install.sh at the top level, so a scripted install path exists, but the README does not document its flags. Treat the docs site as the source of truth for install steps.

Once the client is on your machine and a server is reachable, the workflow is short. Start whatever you are developing on a local port, then run the client against it. The README gives this exact example:

```bash
portr http 9000
```

That command forwards local port 9000 and, per the README, prints a public HTTPS URL, starts the inspector at http://localhost:7777, and begins writing request logs. If you want a stable hostname instead of an assigned one, the README shows the subdomain flag:

```bash
portr http 9000 --subdomain amal-test
```

With a pinned subdomain, the log commands become predictable. These three forms are copied from the README:

```bash
portr logs amal-test
portr logs amal-test /api/
portr logs amal-test --json
```

The first shows the latest logs for that subdomain, the second filters by a URL substring, and the third emits full stored records as JSON. The JSON form is the one to reach for when you want to pipe request data into another tool rather than read it in a terminal.

## Running the server yourself, and the ports you must open

Self-hosting is the point of the project, and the repository provides a docker-compose.yaml with two services. The tunnel service builds from the repository Dockerfile and runs command ["start", "all"]. It mounts ./data into /app/data and declares a healthcheck against http://localhost:8000/api/v1/healthcheck. A postgres service sits behind the postgres profile, so it is opt-in rather than started by default.

The tunnel service publishes three ports: 2222, the admin port from PORTR_ADMIN_PORT defaulting to 8000, and 8001. The SSH port 2222 is the one clients connect to, so it must be reachable from wherever your developers are. The environment variables the compose file passes through include PORTR_DB_URL, PORTR_DOMAIN, PORTR_ADMIN_PORT, PORTR_ADMIN_DEBUG, PORTR_SERVER_URL, PORTR_SSH_URL, PORTR_TUNNEL_USE_LOCALHOST and PORTR_TUNNEL_DEBUG. The README does not document what each variable does or which are required; the .env.template file in the repository is the place to look for the expected set.

The Dockerfile is a three-stage build: Bun compiles the admin web UI from internal/server/admin/web-v2, a golang:1.25-alpine stage builds portrd with CGO enabled and a static link, and the final alpine:3.20 image runs as a non-root portr user. It bundles litestream 0.5.16 and sets PORTR_DB_PATH=/app/data/db.sqlite3 as a default, which the entrypoint overrides when PORTR_DB_URL points elsewhere.

## Where Portr is the wrong tool

The README's own warning is the first limitation: it is not recommended for use alongside production servers. Nothing in the repository describes load balancing, multi-region failover, or an SLA. If you need a stable ingress for customer traffic, this is not that product.

Licensing is the second constraint, and it is structural rather than cosmetic. Portr is licensed under AGPL-3.0. If you run a modified portrd as a network service, the licence's network-copyleft terms are the thing to read before you plan around it. That is a real consideration for a company that would otherwise embed the server in a commercial platform. I am not giving legal advice here; the point is that the licence is a design decision you inherit, not a detail.

A third limitation is documentation depth. The README covers the client workflow well and the server workflow barely at all. There is no documented rollback procedure, no described upgrade path between releases, and no explanation of what happens to in-flight tunnels when portrd restarts. The release cadence is visible in the release list, with v1.0.16 through v1.0.18 landing within about six weeks, but the README does not describe a migration process between them. If you operate the server, you are reading migrations and the entrypoint script yourself.

## How Portr differs from ngrok and from a bare SSH tunnel

The repository's own topics list ngrok-alternative and ngrok-replacement, so the comparison is intended. The difference in approach is hosting. ngrok is a managed service: you install a client, authenticate, and traffic flows through infrastructure you do not operate. Portr inverts that. You run portrd, you own the domain, and the tunnel terminates on a machine you control. The cost is operational: an SSH port to expose, a database to back up, and an upgrade to schedule. The benefit is that request data never leaves your infrastructure, which matters when the payloads you are debugging contain customer data.

Against a plain ssh -R command, the difference is tooling rather than transport. A bare reverse tunnel gives you a port forward and nothing else. Portr adds the inspector at localhost:7777, request replay, WebSocket frame capture, a SQLite log store at ~/.portr/db.sqlite, and a CLI that can query it. Those are the features you would otherwise assemble from a proxy and a log viewer. The trade-off is a heavier client and a server component to maintain, which a one-off ssh -R does not require.

## Maintenance status, upgrade cost and licence implications

The repository is not archived, and the last push was on 2026-09-05. Recent releases are v1.0.18 on 2026-08-12, v1.0.17 on 2026-08-09 and v1.0.16 on 2026-08-03. The project is being released on a short cadence, and the version is still 1.x.

Upgrade cost is concentrated on the server. The Dockerfile builds portrd with the version injected via -ldflags, and the image copies the migrations directory into /app/migrations, so schema changes ship with the binary. The compose file's tunnel service reads PORTR_DB_URL and mounts ./data. If you run SQLite, that directory is your backup target; the bundled litestream binary plus litestream.yml suggests continuous replication is the intended answer, though the README does not document it. If you run Postgres, the postgres profile in the compose file is the path, and the database is the thing to back up.

On licence: AGPL-3.0 applies to the project. If you only run the client against someone else's server, your exposure is minimal. If you modify and host portrd for others, read the licence text in the LICENSE file and get your own advice. The README links to it and does not summarise it.

## Conclusion

Adopt Portr if you are a small team that wants a self-hosted tunnel with a local inspector and CLI-queryable request logs, and you can run the server yourself. Do not adopt it for production traffic: the README states it is not recommended for use alongside production servers, and it is not a fit if an AGPL-3.0 server dependency is unacceptable for your deployment. Before rolling it out, verify the server setup guide at portr.dev/docs/server, confirm which database backend you will run (SQLite via PORTR_DB_URL or Postgres through the compose profile), and check that ports 2222, 8000 and 8001 are reachable from the clients you plan to connect.

## FAQ

### What is Portr and what does it do?

Portr is a tunnel solution that exposes local HTTP, TCP or WebSocket connections on the public internet, using SSH remote port forwarding under the hood. It is designed for small teams that need a public URL for a development server.

### How do I install the Portr client?

The README points to the client installation guide at portr.dev/docs/client/installation rather than inlining commands. The repository also contains an install.sh at the top level, but the README does not document its options.

### How do I use Portr to expose a local port?

Start your local service and run the client against its port, for example portr http 9000. The README states this creates a public HTTPS URL, starts the inspector at http://localhost:7777, and persists request logs locally.

### Is Portr suitable for production servers?

No. The README states that Portr is primarily designed for small teams exposing development servers and is not recommended for use alongside production servers.

## Sources

- [amalshaji/portr on GitHub](https://github.com/amalshaji/portr)
- [License: AGPL-3.0](https://github.com/amalshaji/portr/blob/main/LICENSE)
- [Project website](https://portr.dev)
- [README](https://github.com/amalshaji/portr/blob/main/README.md)
- [Releases](https://github.com/amalshaji/portr/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/amalshaji-portr
