Idea
Can Campfire's Rust port run on Laravel Cloud?
· Ed Grosvenor
We have just been given early access to the Rust runtime on Laravel Cloud. The timing is hard to ignore: a few days ago 37signals released once-campfire-rust, a port of Campfire from Rails to Rust.
The numbers in its README are remarkable. It ships as one binary that replaces Ruby, Puma, Redis, Resque and Thruster. It serves the room page 95 times faster than Rails, sits at 15 MB of memory when idle, and holds 10,000 connected clients in a fifth of the memory Rails needs. It was built to be impossible to tell apart from the Rails app, down to the pixel, and it reads the Rails app's existing database and files so an install can switch over without anyone signing in again.
We have also just finished getting the Rails version running on Cloud. So this is a rare chance to run the same experiment twice, on the same product, in two languages, and see what actually carries over.
What is most likely to trip us up
We've read the source, but we haven't deployed anything. These are our best guesses, roughly in order of how much we expect them to hurt.
SQLite, and this time there's no adapter to swap
This was the known problem last time, and it is bigger here. Cloud doesn't offer SQLite, because the filesystem doesn't survive a deploy. In Rails, moving to Postgres was mostly configuration plus a search rewrite, because Active Record already speaks Postgres.
The Rust port has no such layer. Its data code is a reimplementation of Active Record written directly against SQLite: WAL checkpoints on their own thread, full-text search on SQLite's FTS5, and SQL written for SQLite throughout. Much of the speed comes from exactly that closeness. Moving it to Postgres means rewriting the data layer, not changing a setting. It would also give up the port's promise that Rails can boot on the database it writes. This is the item most likely to decide whether the experiment is a weekend or a month.
Storage that has never heard of S3
The Rails app at least had an S3 service built into Active Storage. The Rust port's storage is disk-only: the same blob keys and folder layout as Rails, with no object-storage code at all. We would be writing an S3 backend, and variants make it awkward, because thumbnails and video posters are generated from a local file and written back.
libvips, but not ffmpeg
Thumbnails come from libvips and video posters from ffmpeg. The port's Dockerfile builds trimmed versions of both from source.
ffmpeg is a solved problem for us. The port runs ffmpeg and ffprobe as separate programs, and loading a pinned ffmpeg build onto Cloud during the build is exactly what our video renderer Slate already does.
libvips is the one to watch. The port doesn't call it as a program. It links against it directly, along with the GLib libraries it depends on, so the library has to be there when the binary is compiled and again when it runs, along with the image-format libraries libvips loads (JPEG, PNG, WebP, HEIF and the rest). That is more than dropping a binary on the path. We expect it to be manageable, whether as a prebuilt libvips for Cloud's platform or a build of our own, but it is the part of the build most likely to fail in a way the logs explain badly.
The build itself
The repository expects its reference/ submodule, the pinned Rails app it tests against, to be checked out for a build. It also compiles with full link-time optimisation and a single codegen unit, which is slow by design. We will find out whether Cloud checks out submodules, and how it feels about a long release build.
Realtime, again
This is the lesson most likely to repeat exactly. The Rust port runs its own Action Cable server inside the binary, and last time Cloud's edge didn't forward websocket upgrades to the app at all. If that holds for the Rust runtime, we'll need the same detour through Cloud's managed Reverb. The good news is that the port keeps Campfire's frontend JavaScript as it is, so the browser half of our Rails workaround, the pusher-js shim and the channel auth flow, should carry over. The server half would have to be rewritten in Rust.
One process by design
Jobs such as pushes and bot webhooks run inside the process, and so does the pub/sub that fans messages out to websockets. That's part of why it's fast, and it also means it's built to be exactly one instance. On Cloud that means no autoscaling. It also leaves open questions about what happens to queued jobs, and to connected clients, when a deploy briefly runs the old instance and the new one side by side.
What we expect to carry over from the Rails run
- Read the dashboard deploy log first. Last time, seventeen deploys failed with the same vague status before we found the real reason in the one place that showed it. That will be step one.
- IPv6 probably won't bite twice. Cloud's network is IPv6-only, and that cost us most of the Rails attempt. The Rust port's public listener already binds every address, IPv6 included. Its internal app port binds loopback by default, which is worth checking once we know how Cloud routes traffic to a Rust app.
- The Redis trap disappears. There is no Redis in this stack at all, so the managed Valkey restriction that broke Action Cable last time can't happen.
- TLS needs thought, not the same fix. Last time, disabling SSL behind Cloud's proxy broke every form. The Rust port does forgery protection differently, checking the browser's
Sec-Fetch-Siteheader instead of Rails' CSRF tokens, so that exact failure may not happen. The right setting still has to be worked out, and the port also has its own Let's Encrypt handling, which must stay off. - The S3 checksum clash will probably be back. If we write the storage backend with the AWS SDK for Rust, it also adds default checksums to uploads, and Cloud's S3-compatible storage rejected exactly that from the Ruby SDK. The same
when_requiredsetting exists in Rust. - Keep the secrets stable.
SECRET_KEY_BASEand the VAPID keys work the same way, and rotating either one signs everyone out or breaks push.
What success looks like
The same bar as last time: two people chatting in real time, attachments in a bucket, search working, and everything surviving a redeploy. This time we also want the port's headline number checked on Cloud: how much less the Rust version costs to run for the same room.