Distributing Rails Apps as Self-Hosted Software with ONCE
Ruby on RailsKevin 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.
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.
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.
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.
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.
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.
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.
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.
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.
Four Requirements for a Distributable App
Knowing how ONCE runs apps makes it clear what an app must provide. McConnell lists four requirements:
- It must be packaged as a Docker image, because that is how ONCE launches it.
- It must serve web traffic on the default port 80, where the proxy sends traffic.
- It must keep all its data inside a particular path, where the persistent volume is mounted.
- It must have a health check endpoint, so the tooling knows when the app is ready and can start sending it traffic.
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.
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.
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.
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.
[music]
Hi there. I hope everyone's having a nice conference so far. This talk is distributing Rails applications with ONCE, and so it's really a talk about three things. It's about distributing applications, or specifically distributing applications to be run in a self-hosted style as opposed to running them as SaaS that you might normally do. It's also a talk about Rails because we'll look at some of the specific features in Rails and see how they fit into this self-hosted style. And it's a talk about ONCE. ONCE is a tool that we built to try and make this whole process as easy as possible.
The context for this talk really came from time that we spent at 37signals building and shipping a series of apps that we designed to be self-hosted in this way. We learned quite a lot from going through this process, both from building and shipping them and talking to customers who were using them. And that's what went on to inform the tooling that we built and the context to them that I'll be talking through in this talk.
To start with, I just wanted to look at a couple of different distribution models so we can compare the details of them. And starting with SaaS because I think that's the thing that most of us are most familiar with.
The way I think about SaaS is it really needs two things in order to work. You need some kind of infrastructure, which could be a rack of servers, a bunch of cloud resources or something like that, and then you need a bunch of customers. And so SaaS works by putting your software on that infrastructure and letting your customers access it.
The self-hosted approach spins everything around a bit. And so the way I think of this is that rather than starting with infrastructure, you start with a customer. You can give that customer a copy of your software, which they run on their own hardware, and then they can use that along with their group of people, whoever they are. So that might be their company, their conference, their family or whatever it is they need. When the next customer comes along, they do the same thing, and now you have two installations out in the wild. More customers come along and you get more installations, and eventually you end up with a picture that looks a bit more like this, where you have tons and tons of these tiny little individual installations all over the place, each one running on a different bit of hardware and each one serving a different set of customers.
When you put these models side by side and look at some of the characteristics of them, I think you often find that something that's a strength for one turns out to be a weakness for the other, or vice versa. There's not necessarily a better way to do this. It's just two different models that serve different purposes.
And the most obvious place to start, I think, is the question of who's running the infrastructure. So in SaaS, you run the infrastructure yourself. That can have some advantages, like you have a lot of control over it. So if you do something like find a bug in your app and ship a fix for it, you know that all of your customers are getting that bug fixed right away, which can be nice. The less good part about running your infrastructure is the fact that you're the one who has to pay for it. So now you have to buy all the machines or rent the cloud resources. You have to pay the bandwidth bills. You probably have to devote somebody's time, your own time or someone else, to look after this stuff, and that can get expensive, right? So for a big SaaS application, the running costs can be fairly considerable.
Whereas in the self-hosted model, every customer brings their own hardware. You don't have to set that up. You don't have to pay for it or look after it. So there's a lot less that you have to do and a lot less you have to pay for.
There's also a big difference in the split of responsibility for data between these two models. So in the SaaS setup, all the data that gets gathered in your application when people are using it is going to land on your servers, and now you're responsible for taking care of that data properly. So making sure that it's kept securely, it's not going to be hacked and stolen. Making sure you have backups so that if something goes wrong, you can recover people's data.
But in the self-hosted style, that's another thing that the customer does for themselves. So now you don't have the responsibility that goes with that, which can be quite freeing. It can also be advantageous for the customer because, from a privacy point of view, they can keep their data somewhere that no one else can get to. They can also keep their data physically near themselves so they'll have faster access to it.
One thing that SaaS does really well, though, I think, is you get really good visibility into how your application's working. Since all the requests into your app are coming into your infrastructure, you can instrument them. You can see your app logs. You can see exception reports and so on. And so you can see in great detail what's working well in your app and what isn't working well in your app, which can be really valuable, and it can help you decide where you should be spending your time if you want to improve your product.
On the self-hosted side, you don't really have that at all. People are going to be installing your software on machines that not only do you not have access to them, you won't even know where they are. And so the only way you could know how your software works there would be to have some kind of telemetry. That's probably something you're not going to be that comfortable doing. It's probably also something that your customers would not be very comfortable with you doing. And so we found it's better just to stay away from that and just accept the fact that you don't have the visibility that you would have with SaaS.
Scaling is something that can be quite complicated at times with SaaS, as I'm sure a lot of you have run into in your jobs. As your application gets bigger and more popular, you may run into situations where you have to engineer your way around some kind of scaling problem. So you might have to do things like split things up into different services or shard big data sets into smaller portions that are more manageable, rethink some algorithms you're using or something like that. And as a programmer, that kind of stuff can be fun, but I think from a business point of view and a product point of view, it's not really ideal that you have to do that. One of the downsides is that it can end up with a lot of complexity leaking into your application, and it's complexity that only exists to get around these scaling problems.
Something that's nice about the self-hosted environment, on the other hand, is that scaling problems just go away because your application only needs to be able to work well enough to serve the biggest individual customer. And for sure you'll have some customers that are bigger than others, but it's still likely that even your largest customer is still significantly smaller or orders of magnitude smaller than that total aggregate workload that you would have had if you were running it in SaaS.
This is something that we definitely saw with Campfire. For example, when we were working on Campfire early on, we did some tests and measurements to see what would it take to make sure it performed well in the biggest installation that we thought someone would reasonably want to use that product for. And what we found was pretty much a vanilla Rails application running SQLite on a modest single server can serve a surprising amount of traffic and was easily enough to serve what we imagined to be a very big installation. And so we got to avoid any of the complexity that might have gone on if we were doing it SaaS.
Another thing that I think SaaS does much better, or is much easier in the SaaS world, is that it gives you much more control over who's accessing your software. So if you want to do something like charge a subscription or require some kind of conditions about sign-up before people can use it, it's easy to do because, again, all those requests are coming into your infrastructure, and you can gate that traffic on anything you want to gate that traffic on.
In the self-hosted model, you have to give up quite a lot of control there. So you can still charge money for software. You can still wire things up to work in a subscription model and so on if you want to. But really, once you've given someone a working copy of your software, the ship has sailed a little bit in terms of how much you could then restrict what they would go on to do with it. You can try and fight a bit, but I think really you have to just accept that there's less control in that model.
And then the last point I wanted to mention here, but I think for the purposes of this talk it's the more interesting one, is just the difference in customer effort that there is between these two models. So in SaaS, this is always super easy for customers, right? They don't have to think about software and hardware. They just go to your product and sign up and start using it. This is pretty much what SaaS was invented for, and it works really well. On the self-hosted side, it's a bigger ask now because now you can give software to someone, but you're also telling them that they have to install it. They have to look after it and make sure it keeps working.
It can be a bit daunting for people who are not used to it. But I will say that what we found in our experience is that this is really more of a problem of perception than a practical problem. You can make it easy for them to do this, and as long as you communicate to people what the steps are going to be involved, then most people won't run into problems.
And so again, using Campfire as an example for this, we sold thousands of copies of Campfire to lots of different people, and we offered support with that. So if anyone had trouble getting it running, they could contact us, and almost nobody did. The support requests that we saw come in were much more often just about product questions or maybe people who wanted to run it in an environment that it wasn't really designed to run, or something like that, and they wanted to talk through that. But almost nobody contacted us to just say they were having a hard time running it, which I think is pretty encouraging.
And that's not to say that I think it means that running software yourself is inherently easy. I think it shows that you can make it easy if you build the right tooling. And so that is a handy segue to the second part of this talk, which is to take a look at ONCE.
So ONCE is the tooling that we built to try and simplify this process. We started looking into this way back when we were working on that first set of applications that I mentioned because it seemed to us like we knew we were going to need to make this a really easy thing for customers to do. Otherwise, the whole premise of making self-hosted software just wasn't going to be viable, right? If it was daunting for people, they weren't going to do it.
And the metaphor and model that we kept coming back to whenever we thought about software installation was old video game consoles, particularly the kinds where if you bought a new game, it came on a physical cartridge, and all you had to do to use that game was just stick that cartridge in the console and it would work. Super simple. And so we thought the closer we could get to that kind of feeling or the spirit of that, the better. Of course, running web applications is probably never going to be quite as simple as that just because we have concerns to deal with a cartridge-based game didn't have. So things like network connectivity or managing the data that occurs over the time you're using it or something like that. But still, it was a goal to aim for to see how close to that we could get.
And I think the main thing we took from it was what you really want is to give someone a way to install your software that's going to one-shot it. You want just a single command. They can run that command and they're done, and everything's working. You don't want to give people a series of steps to follow or things that they have to learn.
We could almost get to that. But the one part that came up when we looked at it was when you're installing web applications, it turns out you pretty much always want to know what hostname or domain name someone's trying to run that software on. Because usually they're going to run these applications so they're accessible over the internet. So we'll need to know things like what name to put on their TLS certificate. If someone's running multiple applications on the same server, then requests are going to come in for all those applications. We need to know how to route them, again based on their names.
So we had two choices. You can either have the thing that nearly one-shots the installation but then leave something that's not properly configured because we don't know the name, or we can have that process just ask them the question at the start. And we found that the second one is much easier for people because then you get prompted for the one bit of information it needs, and everything else just gets taken care of automatically.
So the format we landed on for this command you can one-shot is just the good old curl something into a shell construction. I think these are a nice way to install software on servers, particularly because it asks so little of the server you're installing onto, right? You just need to have some kind of terminal access. You can do it over SSH. You can use the web console in DigitalOcean or whatever. But if you can get to a terminal, you can paste one of these in and you can start there. And so this is the format we started with. This version will be enough to install the ONCE tool. So if you paste this into a terminal now, it'll install the ONCE tool, sort out any dependencies it needs to, and run the tool so that you could then go on to install some apps.
But that in itself isn't really this whole one-shotting the process we wanted to get to because it doesn't leave you yet with the app running. And to get around that, we have this form of the install where you can specify the app that you want. So ONCE knows a few apps by their short name or alias. It knows what Campfire is, it knows what Fizzy is and so on. But it's not just for those apps. It works with any suitable web application. So for other applications, you can just put in the path to a Docker image, and it will install the application that lives in that Docker image. And this is the thing that, as I say, other than asking the question about the hostname, it can one-shot everything from there.
This I'll make a bit clearer if I could show you an example. So here's a video of me doing a complete installation of Fizzy from start to finish just by pasting in the Fizzy version of this link.
So see, in a moment I'll paste in the link. When I run that, it's going to go and fetch the ONCE tool and boot that up. Here's where I get to enter the hostname. I'm just going to use a localhost domain in this example because I was just trying to run it locally on this machine. But then when you hit install, it will do all the remaining steps. It'll go fetch the container, boot the container, configure it, and you're left with something that's working. And that's the whole install complete. So at this point, Fizzy is running on this machine, can open up in the browser. If I was doing this for real, I could go into that sign-up section and create my first Fizzy account and so on. So that's what the process looks like to use it.
When the installation is finished, you end up back on this screen. And so this is ONCE's main view whenever you have any apps installed. And this has
Two jobs to do really. One is that it acts as a dashboard for what's going on on the machine. So, you can see all the apps you have installed. Here we just have one. You can see some stats about how they're behaving, like how much traffic they're serving, how many visitors you've had in the last 24 hours and seven days, a little bit of what their resource usage looks like, so how much memory they're using. And so that dashboard overview is one of the purposes of this.
Its other purpose is it's the jumping-off point to do anything you need to do to manage these applications going forward. So, there's a bunch of different menus and screens that you can get to either with the mouse or keyboard shortcuts. And it's designed to be something you can explore. And the hope here is it makes it a lot less daunting for people who are new to running their own apps because instead of having that question of, "Where do I start?" it's, "Well, here's where you start." Just open this and you can probably find the thing that you're looking for and do it from inside this.
Under the hood, all that's really happening with this is that we're running the apps as Docker containers. So, ONCE will make a Docker network for each app that you install. It'll boot a container for that app. It'll attach a persistent volume so the application can keep its data in that volume and we can keep it safe between launches of containers. That's the source of, if ONCE takes a backup of an application, it's backing up the contents of that volume.
We run the apps behind a little web proxy so that way it can handle the TLS termination and it can also route requests to the correct app if you have multiple apps running on the same machine.
So knowing how these applications run and what environment they're in, that also helps us start to think about what is required of your application to work in this environment and what you'd have to do if you're building a Rails app that needs to fit into it. So that's our second handy segue that takes us into the third part of the talk.
So if you want to build an application in Rails that you can distribute this way and install with ONCE, there's really four requirements that ONCE makes on the application. So it needs to be packaged as a Docker image because that's what we're using to launch the app. It needs to serve web traffic on the default port 80 because that's where the proxy is going to send that traffic to.
It needs to keep all of its data inside a particular path because that's the path that we'll have mounted that persistent volume onto. So anything that your application wants to keep needs to go in there. And it needs to have a health check endpoint so that the tooling knows when your application's ready and healthy and can start sending traffic to it.
If you're starting a new Rails app that you might want to distribute this way, then the short, pithy answer for what you need to do to make it adhere to these principles is to run rails new because these are all just defaults that Rails already does out of the box anyway. So, a new Rails app already comes packaged for Docker because there'll be a Dockerfile generated when you created the application. The configuration in that Dockerfile will serve web traffic over port 80 because it'll have Thruster installed, which will handle the traffic on that port. That also means you get some of the Thruster nice things like faster asset serving and response caching and so on.
By default a Rails app will store its data in the right place, but this works due to a bit of trickery that ONCE is doing. So we didn't want the ONCE tool to be too Rails-specific, which is why it uses this path of /storage, but Rails already has a convention that locally stored data like SQLite databases and so on will be under this path /rails/storage, and we didn't want to fight that convention either. So ONCE gets around this by just mounting the same volume in both places. So it doesn't matter which of these paths you use. You're still writing them to the same place and either one will work.
And then the health check requirement, that's also something that's just built into Rails. So that means your new vanilla Rails app already fits in this environment.
But there are other things that you'll typically reach for when you're building the Rails app, many of which also work quite well when you're shipping your app this way.
Something that's configured on by default, but which you would want to keep, is automatic database migrations. So you probably know that the Dockerfile that gets generated for new Rails applications uses the entry point to run db:prepare whenever you boot the application container. So that has the effect that if there's any pending migrations that need to be applied in that environment, then when you try to boot the app, they'll get applied before the app starts.
This works really well in that self-hosting environment because if you imagine you've shipped version one of your app, you might have thousands of people who have it installed, who have existing data, and their databases are in a particular state. If you then add some more features that require some migrations to run and you ship a newer version of your Docker image, the ONCE tooling will find the newer version and automatically pull that down and upgrade everybody to the latest version. And in doing that, when it boots the new app container, the migrations will be applied and so everyone's database will be in the right form that you expect it. So this process works really well. We've been doing it this way for all of those apps that I mentioned earlier and it's been flawless.
If you need background jobs in your app, Solid Queue fits in quite well in this environment. So you can run its workers in the app container. It can store its data in SQLite. The default settings that you get there just fit. Same with Solid Cache, and same with Active Storage. All of these can just store the data they need to locally on disk, which would be in that volume, and so they tend to work quite well.
So what all that means is that if you wanted to build a new app and distribute it this way, then the steps you have to follow to go from your new app to someone being able to very easily one-shot that install that we showed earlier is really just a few steps. You have to make your new app, build your features, and then just build and push the Docker image for your app to somewhere that's going to be accessible to people, and then make one of these install commands of that format that references your Docker image in it. You can just then share that command with anybody, and if they paste it into a server, they'll have your app running in no time at all in that semi-managed environment that we looked at.
So that really covers most of ONCE's requirements from your app. I just also wanted to mention a few things that work in the other direction, which is things that ONCE does that your app might be able to take advantage of just to smooth out some parts of the process of working in this way. And there's really two categories of these. There's some environment variables that ONCE sets up for you, and there are also hooks that you can use to make the backup process work better.
So to start with, to go through a few environment variables, the first thing is just to know that ONCE is going to take care of this question of assigning a secret key base for you. Obviously in every installation you do of a Rails app, you need to have a unique secret key base so that secrets that are generated by that application are unique to that environment and can't be reused. So people can't do something like take a signed cookie from one installation of your app and use it somewhere else. So, in this self-hosted environment where you have potentially thousands of these installations, they all have to have unique secrets. ONCE will do this for you. It'll just make a random secret key base for each installation that someone does and will pass it through to the container in this environment variable. Rails already looks here for that. So, you don't actually have to do anything about this. I mentioned it just so that you know that it's been handled. It's not something you have to think about in your app itself.
Similarly, ONCE will create a key pair for use with web push. So if your application needs to send push notifications, you can do that using the web push standard. But you'll need some unique credentials, again unique for each installation. And so in case that's something you're going to do, ONCE proactively makes a key pair for you and passes it through in the environment. If you don't use web push, you can just ignore them. But if you do use web push, then it's already there and configured and it makes it easier to do.
If your app has to send email, this one can be a bit fiddly to set up. So, you're not going to be able to have your app just send email from whatever random server someone's going to install it on. Or at least you could, but chances are all that email will just get flagged as spam and not actually get delivered. What's likely to work better is to refer people to use some kind of third-party service to send email through, like Mailgun, SendGrid, Postmark, one of those folks. And all of those will give people some credentials that they can then use to configure their app to send through.
So to make that process a little bit easier to set up, ONCE has a settings screen called Email Settings where someone can just open that up and fill in the form with the details that they got back from the email provider. And if they do that, those will also get passed into your app as a set of environment variables like this. We have Fizzy set up in this way. So if you have an app that needs to send email, you can go and look at the Fizzy repo, and in our production.rb, you'll see the block where we deal with these environment variables. You can just copy that bit into your app and make that work.
And then the last environment variable I wanted to mention is this NUM_CPUS. So ONCE also has the screen where, if someone wants to, they can restrict the amount of resources that each app gets to use on a server. So you can limit how much memory it can access and you can limit the total number of CPU cores that it is allowed to use. If someone does that, then ONCE enforces it through Docker, but it will also pass through that NUM_CPUS as the number of cores that your app is supposed to be using into your app. So that if you do something like spawn background worker processes or something like that, like job workers, you might want to choose the number that you spawn there according to how many cores you have access to, so that you're not trying to run far too many processes for the environment. So we just pass that number through the environment variable so it's easier for you to set that up.
And then the other category of things that I want to talk about was just these hook scripts. So I think I mentioned earlier that ONCE has the ability to automatically do backups for you because that's something that most people are going to want set up. It'll take a backup once per day, build an archive that has all the application's data along with metadata and settings that it needs to recreate the installation, and it keeps a month's worth of those backup archives for you.
Mostly we can do this automatically, but there's one wrinkle to it, which is that it's not necessarily safe to just copy data that's being written to, and ONCE doesn't really know what's going on in your application.
An example of where this could come up is if you're using SQLite. A SQLite database is a file, but if you're actively writing to that file, you can't just naively copy that file and expect to get a valid copy because depending on where the page changes landed as the copy was going on, you might end up with a copy that's corrupt. So, ONCE really has two strategies for dealing with this. The first and the simplest is just that we pause the container briefly while we do that copying step. So that way no writes are landing in the file system. We know that what we get is an atomic copy and it's always something that we're going to be able to safely restore from later.
And often that's fine. One of the nice things about these self-hosted environments is that they tend to be small. The amount of data there is usually smaller. The number of people accessing it is fewer. And so if you have a brief pause that happens once a day, there's a good chance this will just fly under the radar and no one will notice it. So in many cases you can get away with it, but it obviously won't fit every situation and it doesn't scale well if you have the kind of app that generates a lot of data where that copy step might actually take a while.
So to improve that, it also supports these application hooks that you can use to work alongside the backup process and make it run a lot more smoothly.
The way this works is that when ONCE is about to take a backup, right before it does the copy, it'll try and invoke a pre-backup script in your container if there is one. If it finds there's one there and it executes and it returns success, then ONCE assumes that your application is taking care of whatever details need to be taken care of such that live copying the file system is fine. And so in that situation, it doesn't do the pause. It just continues to let your app run while it copies all the data into its archive.
So the way we implement this in our apps, and you can look at something like Campfire for an example of this, is that our pre-backup is just a little Ruby script that connects to the SQLite database and uses an API SQLite has called the online backup API that lets you take a transactionally safe copy of the database into a separate file. So it's fine to do that while the database is changing. So our pre-backup hook just does that and makes that safe copy onto the file system so that that exists in the backup.
If you later restore a backup, ONCE will unpack the data into a new volume. It'll get everything set up ready to run your app. Then right before it boots your app, it'll try and call this post-restore. So in our example, since we made that safe copy of the database, our post-restore script just moves that file back into place where the application database would normally be. So when the app boots up, it's running the safe copy.
So that's pretty much everything I wanted to cover. Just wanted to quickly recap to say that maybe self-hosting is something you'd be interested in trying. Maybe you have an idea for an app that you think would work good in this way. If so, please know that we can make the whole process pretty easy. ONCE can deploy most Rails apps just as they are. So it's a very simple thing to try. If you're interested, then I'd love for you to give it a shot and let us know how you get on. Thanks.
[applause]
[music]
[music]
Article published · Updated
