Notes to self

Running Thruster with Rails and Kamal

Thruster is a new zero config proxy from 37signals. Here’s how to add it to an existing Rails projects deployed with Kamal.

Thruster and Kamal

Thruster solves 4 things as a proxy for Puma:

  • HTTP/2 support
  • HTTPS with Let’s Encrypt
  • HTTP caching of public assets
  • X-Sendfile support and compression

This makes sense as it was created to handle self-hosted ONCE products without Kamal. However, we don’t really need all of that if we already run Rails with Kamal. We handle TLS either directly with Traefik or Cloudflare, and offload storage concerns to CDNs.

So do we need Thruster at all?


Thruster still improves serving application assets like stylesheets and JavaScripts thanks to its X-Sendfile support.

When you serve a file from Rails (e.g. with send_file in a controller), an X-Sendfile header is set. This header is processed by Rack::Sendfile and depending on the proxy, it’s served with the proxy or by this Rack middleware.

And that’s where Thruster comes in. Without any specific configuration we can offload sending these files to Thruster.


You can add Thruster as a gem to your Gemfile:

# Gemfile

gem "thruster"

And run bundle install:

Using rails 7.1.2
Using devise-otp 0.6.0
Using invisible_captcha 2.1.0
Using pay 7.1.1
Installing thruster 0.1.4 (arm64-darwin)
Bundle complete! 46 Gemfile dependencies, 171 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

Make sure you haven’t scoped Thruster to just development as this gem has to be available in production.

Since Thruster is written in Go, you’ll need to make sure the right architecture is used. In my Gemfile.lock I have this:

diff --git a/Gemfile.lock b/Gemfile.lock
index c869d96..f0a2a76 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -422,6 +422,9 @@ GEM
     terminal-table (3.0.2)
       unicode-display_width (>= 1.1.1, < 3)
     thor (1.3.0)
+    thruster (0.1.4-aarch64-linux)
+    thruster (0.1.4-arm64-darwin)
+    thruster (0.1.4-x86_64-linux)
     timeout (0.4.1)
     turbo-rails (1.5.0)
       actionpack (>= 6.0.0)
@@ -498,6 +501,7 @@ DEPENDENCIES
   stimulus-rails (>= 0.7.3)
   stripe (~> 10.6.0)
+  thruster
   turbo-rails (>= 0.9.0)
   web-console (>= 4.1.0)


Now that we have the gem in, we edit our Rails Dockerfile. The idea of Thruster is to just wrap Puma with the thrust command:

# Dockerfile
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Change Puma port to 3001 to keep Traefik default 3000 for Thruster
ENV HTTP_PORT="3000" \

CMD ["bundle", "exec", "thrust", "./bin/rails", "server"]

We set HTTP_PORT for Thruster to keep Traefik port 3000 as a default and TARGET_PORT to Puma port.

This also means we now have to boot Puma on 3001:

# config/puma.rb
if Rails.env.production?
  port ENV.fetch("PORT", 3001)

And update position of the arguments in bin/docker-entrypoint:

#!/bin/bash -e

# The position of ./bin/rails server now changed
if [ "${4}" == "./bin/rails" ] && [ "${5}" == "server" ]; then
  ./bin/rails db:prepare

exec "${@}"

Thruster will handle all the requests for Puma from now on.

That’s it!

Work with me

I have some availability for contract work. I can be your fractional CTO, a Ruby on Rails engineer, or consultant. Write me at