Kamal Proxy: health-gated deploys and traffic draining without app cooperation
Lightweight proxy server for Kamal
At a glance
- What is it?
- Kamal Proxy is a small Go HTTP proxy that routes traffic to an application instance only after it passes health checks, then drains in-flight requests from the old instance before the deploy command returns. It needs no support from the application itself.
- Who is it for?
- Kamal Proxy is the right fit for a team running a handful of containerised services on its own machines with Kamal, especially one that wants host and path routing and managed certificates without standing up a full ingress stack. It is the wrong tool for a deployment that needs weighted canaries, header-based routing, or per-request load balancing, since the proxy only knows about whole instances and health checks.
- 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 1 day 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 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A proxy that coordinates deploys instead of load balancing them
Kamal Proxy does one job: it makes zero-downtime deployments easy to coordinate. The mechanism is deliberately simple. You run web applications behind the proxy, deploy new versions of them, and the proxy decides when traffic should move. The key property is that no particular cooperation from an application is required for this to work, which is the difference between this and a deploy strategy that depends on your framework understanding a signal.
The scope is narrow on purpose. This is a tiny HTTP proxy, not a general purpose edge. It has no configuration file, and everything is expressed as command line options on a running instance and as flags on each deploy. It is designed to work as part of Kamal, which provides the wider deployment experience including container packaging and provisioning, but the documentation is explicit that it could also be used standalone or as part of other deployment tooling.
That standalone property is what makes it interesting beyond Kamal. The concept of deploying an instance to a proxy, where the proxy health checks it and then takes over the traffic, is not tied to any particular container orchestrator. What the proxy needs is a hostname and port, and a way to be told when a new instance is ready.
The implementation is Go, and the repository ships a Dockerfile, a Makefile, and a small example directory with a docker-compose file and a Prometheus configuration, so the deployment shape of the tool itself is easy to inspect before you commit to it.
Health checks gate the traffic, and the deploy command waits for the drain
The sequence is the whole product. You deploy an instance with a target in the form hostname:port, and the proxy registers that instance to receive traffic under the service name. It immediately begins running HTTP health checks to ensure the instance is reachable and working, and as soon as those checks succeed it starts routing traffic there.
If the instance fails to become healthy within a reasonable time, the deploy command stops the deployment and returns a non-zero exit code. That detail is what lets a deployment script handle the failure appropriately, and it is the difference between a health gate and a health warning.
The other half is the drain. Each deployment takes over all the traffic from the previously deployed instance, and as soon as the proxy determines the new instance is healthy it routes all new traffic to it. But the deploy command also waits for traffic to drain from the old instance before returning, which means it is safe to remove the old instance as soon as deploy returns successfully, without interrupting in-flight requests.
Put together, those two behaviours are the zero-downtime claim: traffic only moves once the new instance is healthy, and traffic is drained completely from old instances before they are removed. The guarantee is a property of the command's exit semantics, so a script that waits for the command to succeed inherits it for free.
The health check itself is conventional. By default the proxy sends a GET request to /up once per second and treats a 200 response as healthy. Four deploy flags adjust this: --health-check-path, --health-check-port, --health-check-timeout and --health-check-interval. The port option exists for applications that expose health endpoints on a dedicated port, and the timeout and interval are what you reach for when the default one-second cadence does not suit a slow-starting service.
Starting the proxy and deploying the first instance
There is no package manager entry point or configuration file in the documentation, so the project gives you a binary and a command line. To run an instance of the proxy, use the kamal-proxy run command. The default HTTP port is 80, and if that does not suit the machine, the port is an option:
kamal-proxy run --http-port 8080Once the proxy is listening, the first real use is to deploy an instance of your application to it, using the hostname:port format for the target:
kamal-proxy deploy service1 --target web-1:3000The command blocks while the proxy health checks the new instance, and returns only once the new instance is taking traffic and the old one has drained. Run kamal-proxy help run to see the full list of options for the proxy itself.
The example directory in the repository shows the intended composition, with a docker-compose file, a Prometheus configuration and an upstream service, so you can see the proxy in front of a real app before you wire it into your own hosts.
Host and path routing, and the prefix stripping rule
Two routing modes let one proxy serve several applications.
Host-based routing is the simple case. When deploying an instance you can specify a host it should serve traffic for, and the instance then receives traffic only for that host. Deploying several instances, each with its own host, means multiple applications on one server with no port conflicts. The constraint is that only one service at a time can route a specific host, and the second service to try gets the error "Error: host is used by another service" until the first is removed. That is a real operational rule rather than a soft preference, so a host-to-service map has to be tracked somewhere.
Path-based routing is the more interesting one, for applications that split traffic to different services by request path. Mounting a service under a path prefix sends every request beginning with that prefix to it, and other paths fall through to services deployed without a prefix. The default behaviour strips the prefix before forwarding upstream, so a request to /api/users/123 arrives at the upstream as /users/123. Passing --strip-path-prefix=false forwards the original path including the prefix.
The matching rule has a subtlety worth knowing. Path prefixes are matched against the request path after resolving . and .. segments and collapsing repeated slashes, so a request for /other/../api/users is routed to the /api service. When the prefix is stripped the upstream receives that cleaned path with the prefix removed, and otherwise the path is forwarded exactly as it was sent. In other words a path that looks like it avoids a prefix does not, which matters if you rely on a prefix to separate tenants or to keep one class of traffic off another backend.
Metrics, health endpoint noise, and the exclude-metrics-path flag
Metrics are optional and enabled with --metrics-port, and once enabled every request handled by the proxy is recorded in the Prometheus output. The documentation is unusually candid about a problem this creates. High-volume traffic from upstream load balancers or uptime monitors hitting health endpoints can inflate the metrics pipeline and dominate aggregate measures such as request rate, latency percentiles and error rates, which makes the resulting metrics a poor reflection of real user traffic.
This is a trap specific to this design, and worth understanding. The proxy is the thing health checkers ping, so the proxy is guaranteed to receive a stream of requests that has nothing to do with users. A latency percentile computed over a mix of a monitoring scraper and a real customer is a latency percentile for the scraper.
The fix is --exclude-metrics-path, which may be repeated and whose matches are exact:
kamal-proxy deploy service1 --target web-1:3000 --exclude-metrics-path /up --exclude-metrics-path /healthzExcluded requests are still logged, only the Prometheus counters and the in-flight gauge are skipped. And paths are specified as the upstream receives them, so services deployed using stripped path prefixes should give their excluded paths in the un-prefixed form. That last sentence is a small trap: if you exclude /up on a service mounted at /api, the exclusion path needs to match what the upstream sees rather than what the client typed.
Automatic TLS needs the host set first
Kamal Proxy can obtain and renew TLS certificates for your applications automatically. The switch is the --tls flag on deploy:
kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com --tlsThe constraint is deliberate: automatic TLS requires that hosts are specified, so that certificates are not maliciously requested for arbitrary hostnames. That is a sensible guard, since a certificate authority will happily issue for any name you control, and an open proxy that can be talked into requesting certificates for a domain you do not own is a problem for everyone else.
There is a second condition for path-based routing: TLS options must be set on the root path, and services deployed to other paths on the same host will use the same certificate.
Certificates are obtained and renewed by the proxy itself, which means the proxy process needs a place to keep its state. The Dockerfile shows where that lives: a kamal-proxy user is created, a home directory is made for it, and a.config/kamal-proxy directory is created inside and owned by that user, after which the process runs as that user rather than as root.
Building it yourself and where the pieces live
The repository is a conventional Go project with a Makefile, and the targets tell you what the maintainers run. The build target compiles the cmd tree with CGO disabled and a trimpath flag, and stamps the version into the binary from git describe, with the version string passed into the internal/version package through linker flags. The test target is a plain go test across the module, the lint target runs golangci-lint, and the check target runs go vet followed by govulncheck, which is a useful thing to see in a small project because it means dependency vulnerabilities are looked for deliberately rather than by luck. There is also a bench target and a docker target that builds an image tagged kamal-proxy.
FROM ubuntu:noble-20251013 AS base
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /app/bin/kamal-proxy /usr/local/bin/
EXPOSE 80 443
CMD ["kamal-proxy", "run"]The Dockerfile is a two-stage build on golang:1.27.2, which matches the go directive in go.mod, with the built binary copied into a ubuntu:noble base image along with the CA certificate bundle. Ports 80 and 443 are exposed, the process runs as a dedicated kamal-proxy user, and the default command starts the proxy:
On dependencies, the module is small and the choices are legible. cobra provides the command line interface, quic-go and coder/websocket bring HTTP/3 and WebSocket support, prometheus client_golang drives the metrics output, golang.org/x/crypto supplies the TLS pieces for automatic certificates, and stretchr/testify is the test library. The rest are transitive. There is no database, no message broker and no external service in the dependency list, which is consistent with a proxy whose entire state is the set of registered services and their certificates.
The project is MIT licensed, the repository is not archived, and the last push was on 2026-10-15. There are no GitHub releases published, so the version stamped into a binary comes from git describe rather than from a release tag, and distribution is effectively through the container image or by building from source.
Editorial conclusion
Kamal Proxy is the right fit for a team running a handful of containerised services on its own machines with Kamal, especially one that wants host and path routing and managed certificates without standing up a full ingress stack. It is the wrong tool for a deployment that needs weighted canaries, header-based routing, or per-request load balancing, since the proxy only knows about whole instances and health checks. Before adopting, point the health check at an endpoint your application actually serves, confirm your platform lets it bind ports 80 and 443, and test the drain behaviour against a long-running request, because the zero-downtime guarantee rests entirely on that path completing before the old instance is removed.
Frequently asked questions
What is Kamal Proxy and what does it do?
Kamal Proxy is a small HTTP proxy for coordinating zero-downtime deployments. You deploy application instances to it, it health checks them, routes traffic to a new instance only once it is healthy, and drains in-flight traffic from the old instance before the deploy command returns.
How do I run Kamal Proxy and deploy an application to it?
Start it with kamal-proxy run, where the default HTTP port is 80 and there is no configuration file, only options. Then deploy an instance with kamal-proxy deploy service1 --target web-1:3000, and run kamal-proxy help run to see the full list of options.
How do I change the health check that Kamal Proxy uses?
By default the proxy sends a GET request to /up once per second and treats a 200 response as healthy. The deploy flags --health-check-path, --health-check-port, --health-check-timeout and --health-check-interval let you adjust the path, use a dedicated health port, and change the timeout and the check interval.
Can Kamal Proxy serve several applications on one server?
Yes, through host-based routing with the --host flag, where each instance only receives traffic for its own host, and through path-based routing with --path-prefix, which mounts a service under a path. Only one service at a time can route a specific host, and a second attempt returns "Error: host is used by another service".
Does Kamal Proxy get TLS certificates automatically?
Yes, by adding the --tls flag when deploying an instance, together with a host. Automatic TLS requires hosts to be specified so certificates are not requested for arbitrary hostnames, and with path-based routing the TLS options must be set on the root path.
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/basecamp-kamal-proxy)Was this useful?