Thruster HTTP/2 Proxy on Control Plane
Thruster is Basecamp's zero-config HTTP/2 proxy for
Ruby web applications. It provides HTTP/2 support, asset caching, compression, and early
hints. Running it on Control Plane requires settings that differ from a standalone (e.g.,
VPS) deployment, and getting them wrong produces a confusing 502 Bad Gateway with a
"protocol error" message.
This page documents the configuration that works.
TL;DR
- Workload port:
protocol: http(nothttp2). - Dockerfile
CMDruns Thruster:CMD ["bundle", "exec", "thrust", "bin/rails", "server"]. - End users still get HTTP/2; Control Plane's load balancer handles TLS termination.
Why protocol: http and not http2
Standalone Thruster (e.g., VPS)
User → HTTPS/HTTP2 → Thruster → HTTP/1.1 → Rails
(Thruster handles TLS + HTTP/2)Control Plane + Thruster
User → HTTPS/HTTP2 → Control Plane LB → HTTP/1.1 → Thruster → HTTP/1.1 → Rails
(LB handles TLS) (protocol: http) (caching, compression)In the diagram above, protocol: http is the workload port setting that governs the
LB→Thruster hop; caching, compression describes what Thruster contributes in this
path.
On Control Plane, the load balancer terminates TLS and speaks HTTP/2 to the browser —
so Thruster never sees TLS or HTTP/2 on its incoming side. This is the opposite of a
standalone (VPS) deployment, where Thruster itself handles TLS and HTTP/2. Setting
protocol: http2 on the workload port tells the load balancer to expect HTTP/2 from
the container, which Thruster does not emit on that hop, and protocol negotiation fails
with 502 Bad Gateway.
Even with protocol: http, end users still get:
- HTTP/2 to the browser (from the Control Plane load balancer)
- HTTP/2 multiplexing (from the Control Plane load balancer)
- Asset caching and compression (from Thruster)
- Efficient static file serving (from Thruster)
- Early Hints (103) from Thruster (reaches the browser only if the load balancer forwards 103 responses)
Workload template
In .controlplane/templates/rails.yml:
ports:
- number: 3000
protocol: http # Required when fronting Rails with Thruster. Do not use http2.Dockerfile
The container's CMD must launch Thruster. On Control Plane/Kubernetes the Dockerfile
CMD determines container startup — the Procfile is not used (unlike Heroku).
# .controlplane/Dockerfile
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]Troubleshooting
502 Bad Gateway with "protocol error"
The workload port is set to protocol: http2. Change it to protocol: http in
rails.yml, then push the workload spec.
cpflow apply-template rewrites the workload from the template. If you have tuned
CPU, memory, autoscaling, firewall, or other workload fields directly in the
Control Plane UI (or via cpln) without mirroring those changes back into
rails.yml, those edits will be reset. Either reconcile rails.yml with the live
spec first, or change the port field in place:
# Option A — apply the full template (resets any drift between rails.yml and the live spec):
cpflow apply-template rails -a <app>
# Option B — edit only the port protocol in place (preserves UI-tuned fields):
cpln workload edit <workload> --gvc <gvc> --org <org>
# change spec.containers[].ports[].protocol from http2 to httpInspect the current spec before choosing if you're unsure what would change:
cpln workload get <workload> --gvc <gvc> --org <org> -o yamlNote: cpflow deploy-image alone is not sufficient — it only updates the container
image reference and does not modify the workload's port configuration. Run it
after the protocol change has been applied if you also want to ship a new image.
The remaining troubleshooting commands use the raw Control Plane CLI (cpln) rather
than cpflow; see
the Control Plane CLI quickstart
if you don't already have it installed.
Verify Thruster is the process running as PID 1
/proc/1/cmdline stores arguments NUL-separated with no trailing newline, so pipe it
through tr to make the output readable:
cpln workload exec <workload> --gvc <gvc> --org <org> --location <location> \
-- sh -c "tr '\0' ' ' < /proc/1/cmdline && echo"You should see thrust in the output. If you see rails or puma directly, the
Dockerfile CMD is not invoking Thruster.
Test HTTP connectivity through Thruster
This hits Thruster on port 3000 — not Rails directly. A 200 OK confirms the
Thruster → Rails path within the container is healthy.
cpln workload exec <workload> --gvc <gvc> --org <org> --location <location> \
-- curl -s localhost:3000Inspect the workload port configuration
cpln workload get <workload> --gvc <gvc> --org <org> -o json | jq '.spec.containers[].ports'Reference implementation
A working setup lives in the
react-webpack-rails-tutorial
repository — see
.controlplane/templates/rails.yml
on the master branch.