andydunstall/piko: a reverse proxy where the upstream dials out
An open-source alternative to Ngrok, designed to serve production traffic and be simple to host (particularly on Kubernetes)
At a glance
- What is it?
- Piko inverts the direction of a traditional reverse proxy so services in a customer network or on a user device need no public address, at the cost of a cluster that has to learn where each endpoint currently lives. The engineering is careful, the test strategy is visible in the Makefile, and every release so far is pre-1.0.
- Who is it for?
- Adopt piko when your services cannot be reached inbound: a tool inside a customer network, a bring your own cloud deployment, or anything running on a user device, because the outbound-only tunnel is the whole reason to use it over a stock reverse proxy. Do not adopt it if you need a raw TCP client that is not piko, since the README states raw TCP connections cannot identify their target endpoint and must go through piko forward.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The connection is dialled from the wrong side, on purpose
A traditional reverse proxy is configured with routing rules and then dials the services behind it, which means those services must be discoverable and must expose a port the proxy can reach. Piko reverses that. Your upstream service opens an outbound-only connection to the Piko server and declares which endpoint it is listening on, and Piko forwards incoming traffic down that established connection. The stated consequence is that a service can live anywhere, including a customer network, a bring your own cloud deployment, or a device you do not control, as long as it can open a connection to the server. The second consequence is less obvious and more interesting: because the upstream announces itself rather than being discovered, the proxy needs no static configuration for endpoints at all. An upstream picks the endpoint name it wants to answer on, and that choice is the configuration. So the routing table is built at connect time out of whoever is currently connected, which is a fundamentally different object from a config file, and every operational property of piko follows from that.
Endpoints are a runtime fact, and duplicates are load balanced
The endpoint model is worth stating precisely because it is where the interesting behaviour lives. An endpoint is a name, not a host and not a port. When several upstreams listen on the same endpoint name, requests are load balanced among the available upstreams, which means scaling out is a matter of starting more processes with the same endpoint argument and nothing else. There is no registry to edit and no server-side list to keep in sync. You can open a listener with the agent, which supports both HTTP and TCP, and the same command shape covers both protocols:
piko agent http my-endpoint 3000That example listens on the endpoint named my-endpoint and forwards to port 3000 on the local machine, so the endpoint name and the actual port are decoupled. The second route is the Go SDK, which lets an application listen directly using a standard net.Listener rather than running a separate agent process beside it. That is a real architectural choice with an operational consequence: the agent model keeps your application unaware of piko and lets you tunnel a binary you did not write, while the SDK model removes the extra process and puts a piko dependency inside your binary. Both are supported, and the README treats them as equivalent entry points rather than arguing for one.
HTTP identifies its endpoint in the request, TCP cannot
Piko is a transparent HTTP(S) proxy, and the request itself says where it is going. Two mechanisms are supported, and the choice is between DNS convenience and DNS avoidance. The first is the Host header, where piko takes the first segment as the endpoint ID, so with a wildcard domain at *.piko.example.com a request to foo.piko.example.com goes to whatever is listening on endpoint foo. That is the nicest arrangement to operate and it costs you a wildcard DNS record and a certificate strategy to match. The second avoids all of that by hosting at a single name and passing a header instead:
x-piko-endpoint: fooTCP is the awkward case, and the README is direct about why. There is no way to identify a target endpoint when connecting with raw TCP, because a TCP connection carries no application data before you have already connected. So you cannot point a plain TCP client at the server; you must first connect through piko forward, which listens on a local port and maps it to a named endpoint:
piko forward 3000 my-endpointPiko forward can also authenticate with the server and forward over TLS, and the Go SDK offers the same mapping as a net.Conn. This is the sharpest limitation in the project, and it is a design consequence rather than an omission: anything that can only speak raw TCP to a fixed port is not addressable through piko without an extra client of its own.
How a cluster learns that an endpoint moved to another node
The production story is a cluster of server nodes, and the mechanism for it is specific enough to be worth reading twice. Say an upstream is listening on endpoint E and has connected to node N. Node N notifies the other nodes that it holds a listener for E, so any node receiving traffic for E forwards it to N, and N forwards it down the outbound-only connection. If node N fails or is deprovisioned, the upstream's listener reconnects to a different node, and the cluster propagates the new routing information. So failure recovery is handled by the upstream reconnecting, and the cluster's job is to keep telling each other where things are. What the README does not say is how quickly that propagation completes, and for a proxy promising zero-downtime deployments that gap is the one to measure on your own infrastructure. A request that arrives in the window between a node failing and the cluster learning where E went has nowhere correct to go. Observability is where you would look for that: there is a Prometheus endpoint, access logging and a status API, so the failure mode is intended to be diagnosable after the fact rather than prevented outright.
The Makefile shows two test suites and where the version string comes from
Everything about the build is in the Makefile, and the first target tells you something about releases. The version is taken from git describe and the image tag from the current commit, then stamped into the binary at link time:
mkdir -p bin
go build -ldflags="-X github.com/andydunstall/piko/pkg/build.Version=$(VERSION)" -o bin/piko main.goSo a build from a dirty tree or a shallow clone produces a version string that is whatever git describe returns, which is worth knowing before you file a bug with a version in it. The test story is the more useful part. There are two suites, not one:
go test ./... -v
go test ./tests/... -tags system -v -count 1The first is the ordinary unit run over every package. The second is gated behind a system build tag, lives in a top-level tests directory, and forces a fresh count, which is the shape you give a suite that starts servers, opens tunnels and asserts on traffic rather than on functions. There is also a short variant of the system suite, a coverage target that writes a profile and opens it in a browser, and a lint target that runs go vet and then golangci-lint. For a proxy whose hardest failures are network failures, having a separate system suite behind a tag is the right structure, and the -count 1 flag suggests the author was thinking about cached results in a suite where timing matters.
A proxy for production traffic that has never shipped 1.0
The release history is three tags in a little over a year: v0.8.1 in August 2025, v0.9.0 in January 2026 and v0.10.0 in May 2026. The gaps are a few months each, which is a normal cadence, and the repository is not archived with the last push on 2026-09-22. That last date is the detail to hold onto, because it is more than four months after the newest tag, so the default branch is carrying work that no release contains. Combine that with the two design goals stated at the top of the README, production traffic through a clustered server with zero-downtime deployments, and you get a project asking for production trust while still signalling pre-1.0 instability. That is not a contradiction, and plenty of infrastructure tools live in that state for years, but it does change how you deploy it. Pin a tag if you can, read the diff from v0.10.0 to main before taking a commit, and do not assume the zero-downtime claim is a tested property of your topology. The licence is MIT, which is the permissive part and the least interesting one here.
The README is a signpost, and the alternative it names is Ngrok
Every operational detail lives in the wiki. Getting started, the server, the agent, piko forward, the Go SDK, how piko works, Kubernetes deployment and observability are all wiki pages, and the README is a table of contents with about a page of substance. That is a deliberate choice for a project with this many protocol details, and it is also a real cost: nothing in the repository tells you how to install the binary, what configuration keys the server takes, or how piko authenticates its clients. The comparison the README offers is with Ngrok, and the difference it implies is ownership of the path. With Ngrok your traffic leaves your network through somebody else's relay. With piko you run the relay, put it behind your own load balancer, and the traffic never leaves infrastructure you account for, which is the reason to accept the extra work of running a clustered server. The honest counterweight is that a hosted relay needs no operations from you at all. Where piko is the wrong tool is anything where a raw TCP client must connect without a piko-aware helper, and anything where you would rather not run and monitor a stateful cluster to reach one service.
Editorial conclusion
Adopt piko when your services cannot be reached inbound: a tool inside a customer network, a bring your own cloud deployment, or anything running on a user device, because the outbound-only tunnel is the whole reason to use it over a stock reverse proxy. Do not adopt it if you need a raw TCP client that is not piko, since the README states raw TCP connections cannot identify their target endpoint and must go through piko forward. Before you commit, weigh the version line: the newest release is v0.10.0 from 2026-05-08 while the last push was 2026-09-22, so you are running code ahead of any tag, and the clustering and zero-downtime claims are design goals rather than documented test results. If you do run it, build with make piko and run both make inline-test and make system-test before changing anything.
Frequently asked questions
How do I build andydunstall/piko from source?
The Makefile has a piko target that creates a bin directory and builds main.go, stamping the version from git describe into the binary with a linker flag. The output is bin/piko, and the same Makefile also has an image target that builds a Docker image from build/Dockerfile.
How does piko route an HTTP request to a specific service?
The request carries the target. Piko reads either the Host header, using the first segment as the endpoint ID, or an x-piko-endpoint header, which avoids needing a wildcard domain. With a wildcard at *.piko.example.com, a request to foo.piko.example.com goes to whatever listens on endpoint foo.
Can I use piko for plain TCP traffic?
Yes, but not by connecting raw TCP to the server. The README states there is no way to identify a target endpoint when connecting with raw TCP, so you must go through piko forward, which maps a local port to a named endpoint, or use the Go SDK. The agent also supports TCP upstreams.
What is the latest release of andydunstall/piko?
v0.10.0, tagged on 2026-05-08, following v0.9.0 in January 2026 and v0.8.1 in August 2025. The last push to the repository was on 2026-09-22, so the default branch is ahead of that tag.
How does piko handle a server node failing while an upstream is connected?
The upstream listener reconnects to another node, and the cluster propagates the new routing information to the other nodes so traffic for that endpoint reaches the new node. The README does not state how quickly that propagation completes.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/andydunstall-piko)