The following is a comparison between what was deployed previously and what is deployed now, the exact same app.
| Compiled (Spinel) | Kamal + Docker | |
|---|---|---|
| Disk | 2.6 MB | 1 GB |
| Requests/sec | ~5,000–15,000 | ~300–1,000 |
| Memory (idle) | ~15–30 MB | ~250–400 MB |
The whole thing is deployed on the cheapest DigitalOcean Droplet. I have many apps there, and the main reason for doing this was the disk space and the memory usage when idle. A blog is a read heavy application, so the req/sec doesn't matter that much, because reads will be hitting the cached pages in the reverse proxy.
Transpiling And Compiling
Matz (Ruby's creator) started working on Spinel earlier this year. Spinel is a compiler for Ruby in the traditional sense: instead of running Ruby bytecode in the Ruby VM or doing just in time compilation, Spinel first transforms Ruby code into optimized C code and then compiles that code to a binary. The output is a small executable, like the ones produced by most compiled languages.
Unfortunately we can't compile a Ruby on Rails application with Spinel directly. Ruby on Rails is a huge framework with a lot of metaprogramming, and the type of metaprogramming that exists in Rails, runtime metaprogramming, requires an interpreter, so that code can't be compiled directly.
Roundhouse
Roundhouse is a very ambitious project created by Sam Ruby. Essentially it's a transpiler from Ruby on Rails to basically any modern programming language (Rust, Go, Swift, etc.) and of course Ruby, transforming a Rails app into the exact same app without Rails. You see where we're going with this... I recommend checking Sam Ruby's blog if you want to know more about Roundhouse: https://intertwingly.net/blog/. So first we transform the Rails app into a Ruby app, a much simpler app, and then we compile it with Spinel. We could transform the Rails app into a Rust app and compile that, but I don't recommend it. I had issues with Roundhouse; it's not perfect, it's new and under constant development, and you'll probably need to tweak the transpiled Ruby code to make it work. It's much easier to change Ruby code than Rust code, not only because it's a much simpler language, but also because if you're reading this you're probably a Ruby developer :). After transpiling the Rails code into Ruby code, you compile the Ruby code with Spinel and get your binary.
The Resulting Binary
The resulting binary is small. It's smaller than a Go or Rust binary without any optimization. For this blog it's still much slower than the Rust binary, about 6x slower, which is pretty good, and it's probably going to get better since the Falcon server (a much faster multi threaded Ruby server) was just added to Roundhouse, the main thing that makes it slower is the GC and memory allocation I would image that's the reason Spinel is using jemalloc instead the of default C malloc, but don't quote me on that. I haven't tested the new server yet. The resulting binary has all templates embedded in it. If you link static files like JS and CSS files, then you'll also have those. In my case, just for ease of deployment, I added all the CSS and JS of this blog in the templates themselves, so the resulting app is just two files, the binary and the database, which is pretty neat.
True Parallel Threads in Ruby, No GVL
Ruby doesn't have parallelism, it has concurrency. It can execute multiple threads, each one at a time, but it holds a global VM lock(GVL) when jumping between them, so it's never executing two things at the same time, unless you're using the newish Ractors feature, but using Ractors is more complicated than using threads.
Compiling with Spinel removes the GVL because we're not executing VM bytecode anymore, which makes code compiled with Spinel truly parallel, which is awesome, now you can use Threads in Ruby and produce truly parallel code, I tried, it works, it's awesome :). Ofc now you need to be careful when mutating objects in threads if you're compiling with Spinel.
Deployment
Deploying is very fast and easy: compilation takes 1 minute and the deploy just 10 seconds, and the app is ready. The app runs on Ubuntu with a systemd service to restart the server if it crashes. There's no dedicated application server; to run the app I just need to execute the binary and that's it. Of course there are some downsides. For example, I don't have access to a Rails console, or any other Rails utility, in the production box. Besides the binary and the systemd service, I also have a Kamal proxy pointing to the binary's server and that's it, a pretty simple setup. Migrations are a little bit tricky, Spinel produces a seed sequel file based on that file I identify the new table against the sql on production and create the missing table, also drop the removed ones, it's been working fine so far. Another issue is debugging. You can easily debug a live Rails application, but with a compiled one you have to rely much more on logging. Still, the advantages around deploying more than make up for that.
Compilation
This is the code I use to compile: https://gist.github.com/0000marcell/28a322ff13edf0cd5c93d3e5928d8d64
Pretty straightforward, it should work with most Rails applications. The tricky part is removing the metaprogramming from your app. Roundhouse removes Rails during the transpilation, but you need to manually modify your app. The good thing is that Roundhouse can point you to the things you need to modify: if you run roundhouse check ./app/* you can let your AI do that and check the results. Even after the transpilation you might encounter issues in your app, silent issues. I didn't have a lot of issues, and all of them were related to APIs unsupported by Roundhouse. For example, one of the issues I had was button_to not supporting Hotwire data forms. It failed silently, which is a big problem: the application compiled, but then when I clicked a button using that API it crashed. The fix was to manually change the transpiled Ruby code and compile again. That's why I said it's not a good idea to transpile to a language you don't know. When the application crashed it was immediately restarted by the systemd service, since it's a very small executable it starts super fast.
Development
I develop the app normally with standard Rails. Every issue that I had I added to AGENTS.md and then asked the AI to check the diff. I also run roundhouse check often during development and instruct the AI to do so as well; it works like a linter. Roundhouse has an MCP and an LSP server, but I haven't tried those yet.
My Thoughts About The Whole Thing
Being able to write high level Rails code and then produce a very low footprint binary is incredible, it feels like cheating. It's very exciting but still very early. I tried compiling a more complex application and it worked, it always does, but I kept finding issues with the app, so this is definitely not ready for production apps yet. Still, you should definitely try it out on your own apps, especially small ones, where it's easy to check all the functionality before deploying.
System Tests
Maybe system tests can help here, because then you can test the application binary and make sure everything works, or at least the most critical paths. But system tests have a huge cost, so I'm not sure.