# pgrok: a self-hosted ngrok alternative with SSO-gated stable subdomains

> pgrok is a multi-tenant HTTP/TCP reverse tunnel built on SSH remote port forwarding. It is meant for small teams that already own a domain and an OIDC provider, and it deliberately stops short of production guarantees.

**pgrok/pgrok** — Poor man's ngrok - a multi-tenant HTTP/TCP reverse tunnel solution through SSH remote port forwarding

- Repository: https://github.com/pgrok/pgrok
- Stars: 3,650 · Forks: 132
- Language: Go
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/pgrok-pgrok

## The two things pgrok refuses to rent out

Stable subdomains and single sign-on are the expensive parts of commercial tunneling, and pgrok's answer is to make you supply both from infrastructure you already pay for. The README is explicit about the target: small teams that need to expose a local development environment to the public internet, and that bring their own domain name and SSO provider. That framing matters. pgrok is not trying to be a general-purpose ingress product. It is trying to remove the step where a community manager, a salesperson or a PM has to understand server operations before they can show someone a local build.

The stated design goal is that copy, paste and run is the best UX for everyone, and the README argues this explicitly against the Awesome Tunneling list: not everyone is a developer who knows about server operations, and requiring those colleagues to fix server problems wastes team productivity. So the product decision is not "cheaper ngrok". It is "the tunnel is provisioned once by whoever runs the server, and everyone else runs one command." The README also says plainly that for individuals and production systems you should just buy ngrok. That sentence should be read as a scope statement, not modesty.

## SSH remote port forwarding is the whole mechanism

The tunnel is not a custom protocol. pgrok rides SSH remote port forwarding, which means the client opens an SSH connection outbound to the server, and the server side exposes a listener that is carried back over that connection. The practical consequence is that the client never needs an inbound port open; only the server does.

The server component is pgrokd. It terminates SSH on port 2222, serves the web UI that hands out tokens and URLs on port 3320, and serves tunneled HTTP traffic on port 3000. It stores state in PostgreSQL, which is why the docker-compose.yml ships a postgres service alongside pgrokd. Authentication is OIDC: you create a client in your SSO provider with the redirect URI http://example.com/-/oidc/callback, and the web UI at http://example.com is where a user authenticates to obtain a token and a URL.

Subdomain allocation is per user and stable. By default each user gets one host such as unknwon.example.com, and when that host is already taken pgrok falls back to a prefix generated with a UUID. The --subdomain flag on the http subcommand lets a user pick the prefix, which turns the host into something deterministic like staging-unknwon.example.com. TCP tunnels work differently: the server assigns a port in the 10000-15000 range, and the README describes that assignment as semi-stable, meaning the same port number is reused when it is still available. Semi-stable is the honest word here. Do not build anything that depends on the port number surviving a restart.

## Installing pgrok and running a first tunnel

The server side comes first, and it needs DNS before anything else. You add an A record for example.com pointing at your server's public IP, and a wildcard A record for *.example.com pointing at the same address. If you use Cloudflare, the README says both records must be DNS only. You then need inbound access to port 2222 from anywhere, and if you plan to use TCP tunnels, inbound access to the 10000-15000 range as well.

The repository offers three server paths: a single binary, a standalone Docker container, or Docker Compose. The Compose file in the repository exposes 3320, 3000 and 2222, mounts ./pgrokd at /var/opt/pgrokd, and reads POSTGRES_USER and POSTGRES_PASSWORD from the environment.

```yaml
services:
  pgrokd:
    image: "ghcr.io/pgrok/pgrokd:latest"
    ports:
      - "3320:3320"
      - "3000:3000"
      - "2222:2222"
    depends_on:
      - postgres
```

Front the server with Caddy, which the README uses to split the two HTTP surfaces: the apex domain goes to the web UI on 3320, and the wildcard goes to tunneled traffic on 3000.

```caddyfile
http://example.com {
    reverse_proxy * localhost:3320
}

http://*.example.com {
    reverse_proxy * localhost:3000
}
```

On the client machine, install with Homebrew or download an archive from the Releases page, then initialize the config. The init command writes pgrok.yml, and by default it lands in the standard user configuration directory: ~/Library/Application Support/pgrok/pgrok.yml on macOS, ~/.config/pgrok/pgrok.yml on Linux, and %LOCALAPPDATA%\pgrok\pgrok.yml on Windows.

```sh
brew install pgrok
pgrok init --remote-addr example.com:2222 --forward-addr http://localhost:3000 --token {YOUR_TOKEN}
```

Run the bare pgrok command, or pgrok http, and the client looks for pgrok.yml in the standard user configuration directory or in ~/.pgrok/pgrok.yml. Use --config to point somewhere else and --debug for verbose logging. On success the client prints a line naming your live URL and the remote address it connected to. As a shortcut, pgrok http 8080 sets the forward address as a positional argument.

## Dynamic forwards, and why they change the shape of a tunnel

Most reverse tunnels map one public host to one local address. pgrok allows path-prefixed rules through the dynamic_forwards key, so a single tunnel can serve a frontend and a backend that would otherwise need two tunnels or a local proxy.

```yaml
dynamic_forwards: |
  /api http://localhost:8080
  /hook http://localhost:8080
```

With that config, requests prefixed with /api and /hook go to http://localhost:8080, and everything else goes to forward_addr, which in the README's example is http://localhost:3000. This is the most useful part of the project for frontend work, because gRPC or REST endpoints on a different local port stop being a separate infrastructure problem.

The matching is prefix-based, which is worth pausing on. There is no mention of regex rules, method matching or header matching, so a path that happens to share a prefix with one of your rules will be routed by that rule. If your frontend also owns a route like /apiary, it will be caught by the /api rule. Keep the prefixes unambiguous.

Config values can also be overridden per invocation on both the http and tcp subcommands: --remote-addr (or -r) maps to remote_addr, --forward-addr (or -f) maps to forward_addr, and --token (or -t) maps to token. That is the escape hatch when one developer needs a different upstream without editing the shared file.

## Where pgrok is the wrong tool

The README states the boundary directly: trying to put this behind a production system will blow up your SLA. Take that at face value. There is no documented high-availability story for pgrokd, no documented clustering, and the tunnel itself depends on a single SSH connection between client and server. If that connection drops, the public URL stops answering until the client reconnects.

The TCP port assignment is the second sharp edge. The README describes the server-assigned port as semi-stable and reused when still available, which is a best-effort property, not a guarantee. Anything that hardcodes tcp://example.com:10086 into a peer's configuration will break the first time that port is taken by someone else. If you need a fixed public port, pgrok does not promise one.

Operational cost is real, and it is the cost the README acknowledges by recommending ngrok for individuals. You are running a Go server, a PostgreSQL database, a Caddy instance, DNS with a wildcard record, and an OIDC client. You are also opening port 2222 to 0.0.0.0/0, and 10000-15000 if you use TCP tunnels. That is a meaningful attack surface for a tool whose stated purpose is letting non-engineers show a local build to a colleague. The README's HTTPS walkthrough points at docs/admin/https.md for Caddy and Cloudflare, and the examples in the main README use plain HTTP for brevity, so the default path in the documentation is not the secure one. Do not ship the HTTP example as-is.

## How pgrok differs from ngrok and Cloudflare Tunnel

The comparison the README invites is with ngrok's enterprise tier, which it describes as a bare-bones alternative to at $65 per user per month at the time the project was founded. The architectural difference is ownership. With ngrok you get a hosted control plane, a managed domain, and an SLA; with pgrok you get a binary, a database schema, and the operational work. The feature difference that matters is stable subdomains plus OIDC gating, which pgrok implements against your own identity provider rather than a vendor account.

Against Cloudflare Tunnel the split is different again. Cloudflare Tunnel is a managed service that terminates at Cloudflare's edge, which means you do not run the ingress server and you do not open port 2222 to the internet. pgrok's model is the opposite: the SSH listener is yours, the Postgres database is yours, and the only third party in the path is whichever OIDC provider you already use. If your constraint is "no new vendor in the request path", pgrok's architecture is the reason to pick it. If your constraint is "nobody on the team wants to run a server", it is the reason not to.

The repository's own framing of the alternative space is the Awesome Tunneling list, and its argument against the tools on that list is not technical. It is that most of them assume the person running the client understands server operations. pgrok's differentiator is the SSO-gated onboarding flow that hands a non-engineer a token and a URL from a web page.

## Licence, releases and what an upgrade actually costs

pgrok is MIT licensed, which places few restrictions on reuse and modification; the LICENSE file at the repository root is the authoritative text, and anything beyond that is a question for your own legal review rather than something this article can settle. The practical licence consideration is not the client but the server: pgrokd is the component you deploy, and MIT means you can modify and redistribute it, including inside a company, without a commercial agreement.

The most recent release listed is v1.7.0, dated 2026-06-02, and the last push to the default branch carries the same date. That is roughly three and a half months before today, which is recent enough that the project is not dormant, but the reader should note that the release cadence visible here is sparse: v1.6.0 and v1.7.0 are one day apart, and a latest-commit-build artifact sits between them. This is a small-project release pattern, not a train with a published schedule.

Upgrade cost is concentrated on the server. The Dockerfile builds pgrokd with a version, commit and date stamped in via ldflags, so the running version is identifiable, and the container declares a healthcheck against http://127.0.0.1:3320/-/healthcheck, which gives you a readiness signal during a rollout. The client is a single binary or a Homebrew formula, so client upgrades are cheap. The expensive part is the database: pgrokd uses GORM with the Postgres driver, and the README does not document a migration or rollback procedure. Before upgrading pgrokd across a minor version, take a dump of the pg_data volume, because the documentation gives you no other way back.

## Conclusion

Adopt pgrok if you run a small team, already own a domain and an OIDC client, and can keep a Postgres instance and port 2222 reachable. Do not adopt it for production traffic or if you want a hosted service with an SLA, since the README says putting it behind a production system will blow up your SLA. Before rolling it out, verify that your SSO provider accepts the redirect URI http://example.com/-/oidc/callback, that ports 2222 and 10000-15000 are open inbound, and that the pgrok.yml written by pgrok init lands where the client looks for it.

## FAQ

### Is pgrok a free alternative to ngrok?

pgrok is MIT licensed and self-hosted, so there is no per-user fee, but you supply the domain, the server, the PostgreSQL database and the OIDC provider. The README frames it as a bare-bones alternative to ngrok's enterprise tier and says individuals and production systems should just buy ngrok.

### What is pgrok used for?

It exposes a local development environment to the public internet through SSH remote port forwarding, giving each user a stable subdomain gated by your SSO over OIDC. It supports HTTP tunnels and raw TCP tunnels via the tcp subcommand.

### Can pgrok be trusted with production traffic?

No. The README states that trying to put this behind a production system will blow up your SLA, and the documentation describes no high-availability setup for pgrokd. The tunnel also depends on a single SSH connection between client and server.

### How does pgrok handle port forwarding?

The client opens an SSH connection to pgrokd on port 2222 and uses SSH remote port forwarding to carry the listener back to the client, so only the server needs an inbound port open. HTTP traffic is served on port 3000 and the web UI on port 3320.

## Sources

- [Issues](https://github.com/pgrok/pgrok/issues)
- [License: MIT](https://github.com/pgrok/pgrok/blob/main/LICENSE)
- [pgrok/pgrok on GitHub](https://github.com/pgrok/pgrok)
- [README](https://github.com/pgrok/pgrok/blob/main/README.md)
- [Releases](https://github.com/pgrok/pgrok/releases)

---

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