Rails on OpenBSD: Motivation and Architecture

Combining the simplicity of OpenBSD with the productivity of Ruby on Rails.

It’s no secret I consider Ruby on Rails to be one of the finest choices for product development, but the choice of OpenBSD might be surprising. I’m explaining that choice here, and hope to convince you to at least have a look at that operating system.

This is the first article in a series of articles on deploying a Ruby on Rails application to a VPS running OpenBSD. Initially, I planned to write one long article, but it became so lengthy that I had to break it up for my own and my readers’ sanity. The series will cover hardening OpenBSD, setting up nginx, Let’s Encrypt, PostgreSQL, building Ruby, deploying Rails, automated encrypted backups, and firewall rules. That’s plenty of ground to cover! In initiatives as large as this, it’s best to start with some motivation and a high-level overview.

Motivation

When I was deploying www.whoishiring.jobs for the first time, I decided to host it on Render to explore alternatives to Heroku. Render’s developer experience was nice, and despite some gotchas around PostgreSQL disk usage, I was pleased with the outcome. Each of the three components (app, worker, and database) cost $7 / month, totaling to $21 per month.

Gotchas? What gotchas!?

I connected the app to a database on the cheapest paid plan ($7 / month) with 1 GB of disk space. I enabled solid_queue but forgot to schedule the removal of old jobs, which quickly used up all disk space. I thought I would be able to connect to the database and truncate problematic tables, but it turns out the database rejected connections.

The end result was that the database and application were down, and the only solution do the problem was re-creating the database. Theoretically, I could have contacted support, but in those circumstances, I simply decided to leave Render at some point in the future.

21 bucks is not a staggering amount of money, but I did have financial concerns regarding scaling. Deploying another project to Render would double the cost to $42 / month. And with three projects, I’d be paying $63 / month. That’s the kind of scalability concern I had, especially for running multiple niche applications.

Hetzner’s cheapest VPS costs $12 / month. I’m no stranger to operating my own servers, so spending that amount on a machine capable of running an arbitrary number of applications was tempting and I finally gave in. Ruby on Rails is my framework of choice, so the only other significant decision was picking an operating system. I strongly prefer cathedrals over bazaars, used NetBSD as my daily driver 20 years ago, and use FreeBSD in my home lab. I hadn’t tried OpenBSD before, but its focus on simplicity and security caught my attention. It felt as if it would fit The One Person Framework Philosophy. Now that I’ve moved the app to OpenBSD, I can confidently say – I made the right call.

Detailed Migration Goals

Before diving into technical details, let’s get specific about my goals for the migration:

  1. Deploying an arbitrary number of applications without increase in cost (to a point, naturally).
  2. Installing and compiling arbitrary software to support those applications.
  3. Conceptual simplicity and coherence.
  4. Strong security that wouldn’t require a lot of effort from me.
  5. Operating other networking services, like a VPN.

Points 3 and 4 were especially important from the perspective of a product builder: I want to spend more time building products, instead of fidgeting with the operating system or worrying about potential intruders. I must admin OpenBSD definitely delivers in that area: it is simple and well-designed.

We’re now ready to turn our attention to the high-level technical vision.

Architecture

Setting up a server takes multiple interrelated steps, so it’s helpful to have a big picture understanding of the desired end state. The diagram below illustrates the major pieces involved.

A diagram showing nginx accepting an incoming user connection, serving static assets from public directories of three apps, and proxying requests to two Puma instances; those Puma instances connect two PostgreSQL databases running under one server process.

The diagram shows a few system processes (rectangles with solid backgrounds): nginx, two instances of Puma, PostgreSQL. The middle rectangle is the file system hierarchy, starting at /home/www, and shows Ruby source code files loaded by Puma and public directories served directly by nginx.

nginx is the sole outward-facing service (other than SSH, which is for internal use only), as indicated by the large arrow leading to it from “User”. nginx is used for three purposes:

  1. Serving static assets like scripts, style sheets or images; this applies to both static websites, like my personal website, and assets deployed along with Rails apps.
  2. Proxying non-static requests to Puma.
  3. Responding to ACME challenges required to obtain TLS certificates via Let’s Encrypt.

Each Rails application runs its own Puma process and connects to a dedicated database. All application databases are part of the same database cluster managed by the same PostgreSQL server process.

Since neither Puma nor PostgreSQL will accept network connections, it makes sense to make them listen only on UNIX sockets. I consider this to be a form of principle of least privilege, which reduces overall risk and complexity.

The diagram lacks a lot of details that will be explained in upcoming articles: locations and permissions of UNIX socket files, operating system accounts for the app, SSH, database backups, off-site encrypted backups, backup rotation, Puma and solid_queue startup scripts, logs.

Not Invented Here Syndrome

After buying the VPS from Hetzner, I immediately started creating an Ansible playbook to provision the server. Unfortunately, I didn’t get far. I hate YAML programming and prefer a real programming language, even as flawed as the POSIX shell. I do like informative progress messages and idempotence, though. Sadly, none of the existing tools fit my preferences, so … I created my own!

Running a single command to provision a server is the magical experience, so I wanted to name the tool “hocus-pocus”. Sadly, that name felt too long for terminal use, which is why I cut it in half and that’s how pocus was born.

pocus is exactly what I want from a tool designed to manage just a few servers. I recommend you have a look at the source code - it can be read in under an hour.

Closing Thoughts

I hope the article made you curious, if not excited, about OpenBSD. The next article will cover base system setup: creating a regular user account, setting up doas (OpenBSD alternative to sudo), setting up accounts for the application, and hardening OpenSSH. If you’d like not to miss the article when it’s out then you can leave me your email or follow me on Twitter.

Enjoyed the article? Follow me on Twitter!

I regularly post about Ruby, Ruby on Rails, PostgreSQL, and Hotwire.

Leave your email to receive updates about articles.