Caddy can proxy gRPC traffic, but it’s not as simple as just pointing it at your service; you must explicitly enable HTTP/2 without TLS (H2C) or configure TLS termination correctly.
Let’s see Caddy in action proxying a simple gRPC service.
First, imagine we have a gRPC service running on localhost:50051. This service expects grpc.health.v1.Health checks and handles some custom RPCs.
Here’s a basic Caddyfile to proxy this service, enabling H2C:
:8080 {
reverse_proxy localhost:50051 {
transport http {
versions h2c
}
}
}
When a client connects to Caddy on :8080 using an H2C connection, Caddy upgrades the connection to HTTP/2 and forwards it to localhost:50051. The versions h2c explicitly tells Caddy to use HTTP/2 without TLS. This is crucial because gRPC requires HTTP/2. Without this, Caddy would default to HTTP/1.1, and your gRPC calls would fail with an obscure error.
If your gRPC service is using TLS (e.g., running on localhost:50052 with TLS enabled), Caddy needs to be configured to speak TLS to it.
:8080 {
reverse_proxy localhost:50052 {
transport http {
tls internal
}
}
}
Here, tls internal tells Caddy to use TLS when connecting to the upstream. Caddy will attempt to use TLS 1.2 or 1.3 and will trust certificates signed by the system’s root CAs by default. If your upstream uses a self-signed certificate or a certificate signed by a private CA, you’d need to configure Caddy to trust that CA.
The real power comes when Caddy handles TLS termination for clients and then forwards the traffic to an H2C upstream, or vice-versa.
Consider a setup where clients connect to Caddy over HTTPS (TLS termination), but the backend gRPC service only speaks H2C:
:443 {
tls your-email@example.com
reverse_proxy localhost:50051 {
transport http {
versions h2c
}
}
}
Caddy will obtain a certificate for your domain (or use one you provide), terminate the TLS connection from the client, and then establish an H2C connection to localhost:50051. This is a very common pattern for exposing gRPC services securely over the internet. The versions h2c is essential here for the upstream connection.
Now, what if Caddy needs to proxy gRPC traffic to another Caddy instance or a load balancer that’s already handling TLS?
:8080 {
reverse_proxy grpc.internal.example.com:443 {
transport http {
tls internal
}
}
}
In this scenario, Caddy on :8080 is acting as a client to grpc.internal.example.com:443. It’s initiating a TLS connection to that upstream. The tls internal directive tells Caddy to trust certificates signed by the system’s root CAs. If grpc.internal.example.com uses a self-signed certificate, you’d need a tls_trusted_ca_certs directive pointing to the CA bundle.
The transport http block is where the magic happens for gRPC. HTTP/2 is a prerequisite for gRPC. When you don’t specify versions h2c or tls, Caddy defaults to HTTP/1.1, which gRPC cannot use. Explicitly setting versions h2c or using transport http { tls ... } ensures that Caddy attempts to establish an HTTP/2 connection to the upstream. Caddy’s HTTP/2 implementation handles the framing and multiplexing required by gRPC, making it a seamless proxy.
When Caddy proxies gRPC, it doesn’t inspect the gRPC messages themselves. It operates at the HTTP/2 level. This means it can handle gRPC streams, bidirectional streaming, and unary calls without needing to understand the Protobuf serialization. The reverse_proxy directive, combined with the correct HTTP version negotiation in the transport block, is all that’s needed.
A subtle point often missed is that if your gRPC service requires specific HTTP/2 settings (like window sizes or connection preface handling), Caddy’s defaults are generally robust. However, for extreme edge cases, you might find yourself needing to delve into lower-level network tuning, but Caddy’s HTTP/2 implementation is designed to be compatible with common gRPC server behaviors.
The next challenge you’ll likely face is managing authentication and authorization for your gRPC services, especially when Caddy is acting as the public-facing entry point.