Distributing Rails Apps as Self-Hosted Software with ONCE

Open on YouTube ↗
Overview

Kevin McConnell of 37signals argues that SaaS isn't the only way to ship a Rails application. You can also give customers a copy of the software to run on their own hardware. His talk covers three things: the trade-offs between SaaS and self-hosting, the ONCE tool 37signals built to make self-hosted installation as easy as possible, and the Rails features that make an ordinary Rails app ready for this kind of distribution. His view comes from building and shipping a series of self-hosted apps at 37signals, including Campfire and Fizzy, and from talking to the customers who ran them.

18 min read
1:09

Two Ways to Get Software to Customers

McConnell describes SaaS as needing two things: infrastructure, such as a rack of servers or a set of cloud resources, and customers. You put your software on the infrastructure and let customers access it.

Self-hosting reverses this. Instead of starting with infrastructure, you start with a customer. You give them a copy of the software, they run it on their own hardware, and they use it with their own group, which might be a company, a conference, or a family. Each new customer creates another installation. Over time you end up with "tons and tons of these tiny little individual installations all over the place," each on different hardware and each serving different people.

When he compares the two models, McConnell finds that a strength of one is often a weakness of the other. He doesn't think either model is better. They serve different purposes.

2:49

Infrastructure, Cost, and Data

The first difference is who runs the infrastructure. With SaaS, you do. That gives you control: if you find a bug and ship a fix, every customer gets it immediately. You also pay for it. You buy or rent the machines, pay the bandwidth bills, and spend someone's time looking after it all, and for a large SaaS product those running costs can be considerable. With self-hosting, every customer brings their own hardware, so there is much less for you to set up, pay for, or maintain.

Responsibility for data splits the same way. In SaaS, all the data your users create lands on your servers, and you have to keep it secure and backed up. In the self-hosted model the customer takes on that job, which McConnell says "can be quite freeing" for the vendor. It can also help customers: they can keep their data somewhere no one else can reach it, for privacy, and keep it physically close for faster access.

Visibility

McConnell says SaaS does one thing especially well: it shows you how your application is working. All requests pass through your infrastructure, so you can instrument them and read logs and exception reports. You can see in detail what works and what doesn't, which helps you decide where to spend time improving the product.

Self-hosting gives you essentially none of this. Your software runs on machines you can't access, and you won't even know where they are. The only way to get visibility would be telemetry. McConnell thinks vendors are unlikely to be comfortable adding it and customers are unlikely to be comfortable accepting it. 37signals concluded it was better to stay away from telemetry and accept the loss of visibility.

5:30

Scaling, and What Campfire Showed

In SaaS, scaling can become complicated as an app grows. Teams may split systems into services, shard large data sets, or rethink algorithms. McConnell admits this work can be fun for programmers. From a business and product point of view, though, it adds complexity to the application that exists only to work around scaling problems.

Self-hosting largely removes this problem. The app only has to perform well enough for the largest single customer. Even the largest customer is likely to be far smaller, possibly by orders of magnitude, than the combined workload a SaaS version would carry.

Campfire is his example. Early in its development, the team ran tests and measurements to see what it would take to handle the biggest installation they thought anyone would reasonably want. They found that a "pretty much vanilla" Rails application running SQLite on a modest single server could serve a surprising amount of traffic. That was easily enough for what they imagined to be a very large installation, so they avoided the complexity a SaaS version might have needed.

Control Over Access

SaaS also makes it easy to control who uses your software. Every request passes through your infrastructure, so you can require a subscription or set sign-up conditions and gate traffic however you like.

Self-hosting gives up much of that control. You can still charge money and even set up a subscription model. But once someone has a working copy of the software, McConnell says, "the ship has sailed a little bit" in terms of restricting what they do with it. You can push back a little, but he thinks you have to accept that this model offers less control.

Customer Effort: More Perception Than Reality

McConnell calls customer effort the most interesting difference for this talk. SaaS is easy for customers: they sign up and start using the product, which is what SaaS was designed for. Self-hosting asks more. Customers must install the software and keep it running, and that can seem daunting to people who haven't done it before.

In 37signals' experience, though, this is "more of a problem of perception than a practical problem." If you make the process easy and explain the steps clearly, most people won't run into trouble. He again cites Campfire. 37signals sold thousands of copies and offered support to anyone who had trouble getting it running, and almost nobody used it for that. Most support requests were product questions, or came from people who wanted to run the software in an environment it wasn't designed for and wanted to talk that through. Very few people said they were struggling to run it.

McConnell is careful about what this shows. He doesn't claim that running your own software is inherently easy. He thinks it shows that you can make it easy with the right tooling.

9:48

ONCE and the Cartridge Metaphor

ONCE is that tooling. 37signals started working on it while building the first set of self-hosted apps, because they knew that if installation felt daunting, people wouldn't do it and self-hosted software wouldn't be viable.

The model they kept returning to was old video game consoles, specifically the ones where a game came on a physical cartridge. You put the cartridge in the console and it worked. Web applications will probably never be quite that simple, McConnell says, because they have concerns a cartridge game didn't, such as network connectivity and data that builds up over time. The cartridge was still the goal to get as close to as possible.

The main lesson they took from the metaphor was that installation should "oneshot" the software. Customers should get a single command that leaves everything working, not a series of steps or things to learn.

11:07

The One Question ONCE Asks

They could almost reach a fully automatic install, but one piece of information was always needed: the hostname or domain name the customer wants to run the app on. These apps are usually reached over the internet, so the tooling needs the name for the TLS certificate. If several apps share a server, it also needs the names to route incoming requests to the right app.

That left two options. ONCE could install almost everything automatically and leave the app not fully configured, or it could ask for the hostname at the start. They found the second option much easier for people: users are asked for the one piece of information the tool needs, and everything else is handled automatically.

12:06

The Install Command

For the command itself, they chose the familiar pattern of piping curl into a shell. McConnell likes this approach for servers because it asks so little of the machine. You only need terminal access, whether over SSH or through something like DigitalOcean's web console. If you can reach a terminal, you can paste the command.

The basic form of the command installs the ONCE tool, sorts out any dependencies, and launches the tool so you can install apps. On its own, that doesn't leave a running app, so there is a second form that names the app you want. ONCE recognizes some apps by short aliases, such as Campfire and Fizzy. It isn't limited to those: for any suitable web application you can supply the path to a Docker image, and ONCE installs the app in that image. Apart from the hostname question, this form handles the whole install in one go.

13:27

Demo: Installing Fizzy from Scratch

McConnell showed a video of a complete Fizzy installation using the Fizzy version of the command. After he pasted the command, it fetched and started the ONCE tool, then prompted for a hostname. He used a localhost domain because he was running it on his own machine. When he chose install, ONCE fetched the container, configured it, and left Fizzy running and reachable in the browser. In a real deployment, he said, the next step would be to go to the sign-up section and create the first Fizzy account.

The Dashboard

When installation finishes, the user lands on ONCE's main view, which has two jobs. First, it is a dashboard for the machine. It lists the installed apps and shows stats for each one, including how much traffic it serves, visitor counts for the past 24 hours and seven days, and some resource usage such as memory.

Second, it is the starting point for managing the apps from then on. Menus and screens can be reached by mouse or keyboard shortcut, and the interface is meant to be explored. The goal is to make running your own apps less daunting: instead of wondering where to start, users open this screen and can probably find what they need there.

15:22

What Happens Under the Hood

Underneath, ONCE runs the apps as Docker containers. For each installed app it creates a Docker network, boots a container, and attaches a persistent volume where the app keeps its data. That data stays safe between container launches, and the volume's contents are what ONCE backs up. The apps run behind a small web proxy that handles TLS termination and routes requests to the right app when several share a machine.

16:20

Four Requirements for a Distributable App

Knowing how ONCE runs apps makes it clear what an app must provide. McConnell lists four requirements:

  1. It must be packaged as a Docker image, because that is how ONCE launches it.
  2. It must serve web traffic on the default port 80, where the proxy sends traffic.
  3. It must keep all its data inside a particular path, where the persistent volume is mounted.
  4. It must have a health check endpoint, so the tooling knows when the app is ready and can start sending it traffic.
16:51

Why rails new Already Fits

McConnell's "short, pithy" answer to how a new Rails app meets these requirements is to run rails new, because Rails already does all of this by default. A new Rails app includes a generated Dockerfile. That Dockerfile's configuration serves traffic on port 80 through Thruster, which also brings Thruster's benefits such as faster asset serving and response caching. Rails also includes a health check endpoint out of the box.

The data path works through what McConnell calls "a bit of trickery" on ONCE's side. They didn't want ONCE to be too Rails-specific, so its data path is /storage. They also didn't want to fight the Rails convention of keeping locally stored data, such as SQLite databases, under /rails/storage. ONCE resolves this by mounting the same volume at both paths, so an app can write to either and the data ends up in the same place.

18:21

Migrations, Jobs, Caching, and Storage

Many other things Rails developers commonly use also work well in this setting. Automatic database migrations are on by default, and McConnell recommends keeping them. The Dockerfile generated for new Rails apps runs db:prepare from its entrypoint every time the container boots, so any pending migrations are applied before the app starts.

He explains why this matters for self-hosting. Suppose you ship version one and thousands of people install it, each with existing data and a database in a particular state. You then add features that need migrations and push a newer Docker image. ONCE finds the new version, pulls it, and upgrades every installation automatically. When the new container boots, the migrations run and every database ends up in the expected shape. McConnell says 37signals has handled all the apps he mentioned this way and that it has been "kind of flawless."

For background jobs, Solid Queue fits well: its workers can run in the app container, it can store its data in SQLite, and its default settings suit this environment. The same is true of Solid Cache and Active Storage. Each can store its data locally on disk, which means inside the persistent volume.

The result is a short path from a new app to a one-command install. Create the app, build your features, build the Docker image and push it somewhere people can reach, then write an install command in the format shown earlier that references your image. Anyone who pastes that command into a server will quickly have your app running in ONCE's semi-managed environment.

20:31

What ONCE Provides Through Environment Variables

McConnell also covered things ONCE does that apps can take advantage of. These fall into two groups: environment variables and backup hooks.

Secret key base. Every Rails installation needs a unique secret key base, so that secrets one installation generates can't be reused elsewhere. For example, a signed cookie from one installation shouldn't work on another. With potentially thousands of self-hosted installations, each needs its own secret. ONCE generates a random secret key base for each install and passes it into the container through the environment variable Rails already reads, so the app doesn't need to do anything.

Web Push keys. Apps that send push notifications through the Web Push standard need credentials that are unique to each installation. ONCE generates a key pair in advance and passes it through the environment. Apps that don't use Web Push can ignore it.

Email settings. McConnell calls email "a bit fiddly." An app could send mail directly from whatever server it is installed on, but that mail will probably be flagged as spam and not delivered. It works better to have people use a third-party sending service such as Mailgun, SendGrid, or Postmark, each of which gives them credentials. ONCE has an "email settings" screen where users enter the details from their provider, and those values are passed to the app as environment variables. Fizzy is set up this way, and McConnell points developers to the block in Fizzy's production.rb that reads these variables, which they can copy into their own apps.

CPU count. ONCE also has a screen for limiting each app's resources, including memory and the number of CPU cores. If a user sets a limit, ONCE enforces it through Docker and also passes the allowed number of cores to the app in a NUM_CPUS environment variable. Apps that spawn background or job worker processes can use it to choose a sensible number of processes instead of running far too many.

24:15

Backups and Application Hooks

ONCE backs up apps automatically, since most people will want that. It takes a backup once a day, creates an archive containing all of the app's data plus the metadata and settings needed to recreate the installation, and keeps a month of archives.

The complication is that copying data while it is being written isn't necessarily safe, and ONCE doesn't know what the app is doing. SQLite is the example McConnell gives: a database is a file, but if it is being written during the copy, you can end up with a corrupt copy depending on where the page changes land.

ONCE has two strategies for this. The simplest is to pause the container briefly during the copy, so nothing is written and the copy is atomic and safe to restore. McConnell says that is often fine, because self-hosted installations tend to be small, with less data and fewer users, so a short daily pause will probably go unnoticed. It won't suit every situation, though, and it scales poorly for apps with enough data that the copy takes a while.

The second strategy uses application hooks. Right before copying, ONCE tries to run a pre-backup script in the container. If the script exists and returns success, ONCE assumes the app has made a live copy safe, skips the pause, and copies the data while the app keeps running.

At 37signals, and McConnell points to Campfire as an example, the pre-backup hook is a small Ruby script. It connects to the SQLite database and uses SQLite's online backup API to write a transactionally safe copy to a separate file, which works even while the database is changing. That safe copy is then included in the backup.

Restoring works in reverse. ONCE unpacks the data into a new volume and prepares everything to run the app. Just before booting the app, it calls a post-restore script. In 37signals' apps, that script moves the safe database copy back to where the application database normally lives, so the app boots using the safe copy.

Closing

McConnell ended by encouraging anyone with an app idea suited to self-hosting to try it. He said the whole process can be made fairly easy and that ONCE can deploy most Rails apps as they are, so it is a simple thing to try. He invited the audience to give it a shot and tell 37signals how it went.