<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://bridgetownrb.com/" version="2.2.1">Bridgetown</generator><link href="https://binarysolo.blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://binarysolo.blog/" rel="alternate" type="text/html" /><updated>2026-09-14T12:32:13+01:00</updated><id>https://binarysolo.blog/feed.xml</id><title type="html">Binary Solo</title><subtitle>A blog about web development, Ruby, Rails, and some other nonsense ...</subtitle><entry><title type="html">Generating self-signed certificates for web services</title><link href="https://binarysolo.blog/generating-self-signed-certificates-for-web-services/" rel="alternate" type="text/html" title="Generating self-signed certificates for web services" /><published>2026-09-10T00:00:00+01:00</published><updated>2026-09-10T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-09-10-generating-self-signed-certificates-for-web-services.md</id><content type="html" xml:base="https://binarysolo.blog/generating-self-signed-certificates-for-web-services/">&lt;p&gt;Private web services such as PostgreSQL or Redis shouldn’t be exposed to the internet. This keeps them secure, as they can only be accessed via an SSH session to the machine they’re installed on, or through a private cloud network.&lt;/p&gt;

&lt;p&gt;However, if you must expose them to the internet, I recommend securing them using self-signed certificates. This way, the connection cannot be hijacked or eavesdropped upon.&lt;/p&gt;

&lt;p&gt;Since these are internal services, there’s no value in using a public certification authority (CA) such as Let’s Encrypt to issue the certificates. We can create our own trusted certificate chain for authentication.&lt;/p&gt;

&lt;h2 id=&quot;creating-the-certificates&quot;&gt;Creating the certificates&lt;/h2&gt;

&lt;p&gt;We need to create a root certificate which will be used to sign all other certificates. This certificate sits at the top of the trust chain, and is used to vouch for all other certificates via a cryptographic signature.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;openssl req &lt;span class=&quot;nt&quot;&gt;-x509&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-newkey&lt;/span&gt; rsa:4096 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-keyout&lt;/span&gt; ca.key &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; ca.pem &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-days&lt;/span&gt; 3650 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-nodes&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-subj&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/O=My Company/CN=My Company Root CA&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;-subj&lt;/code&gt; contains metadata to store in the certificate. &lt;code class=&quot;highlighter-rouge&quot;&gt;O&lt;/code&gt; stands for &lt;em&gt;organization&lt;/em&gt;, and &lt;code class=&quot;highlighter-rouge&quot;&gt;CN&lt;/code&gt; stands for &lt;em&gt;common name&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The private key (&lt;code class=&quot;highlighter-rouge&quot;&gt;ca.key&lt;/code&gt;) generated by this command is the most important secret. It will be used to sign all other certificates. Keep it safe!&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;I usually store such secrets in a password manager like 1Password.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;The certificate (&lt;code class=&quot;highlighter-rouge&quot;&gt;ca.pem&lt;/code&gt;) is our root certificate which we will implicitly trust.&lt;/p&gt;

&lt;p&gt;Next, create a key and certificate signing request (CSR) for a certificate to install on the server:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;openssl req &lt;span class=&quot;nt&quot;&gt;-newkey&lt;/span&gt; rsa:4096 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-keyout&lt;/span&gt; server.key &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; server.csr &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-nodes&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-subj&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/O=My Company/CN=My Server&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And another key and CSR for clients:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;openssl req &lt;span class=&quot;nt&quot;&gt;-newkey&lt;/span&gt; rsa:4096 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-keyout&lt;/span&gt; client.key &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; client.csr &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-nodes&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-subj&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/O=My Company/CN=My Client&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Sign both server and client CSRs with the root certificate:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;openssl x509 &lt;span class=&quot;nt&quot;&gt;-req&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-in&lt;/span&gt; server.csr &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-CA&lt;/span&gt; ca.pem &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-CAkey&lt;/span&gt; ca.key &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-CAcreateserial&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; server.pem &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-days&lt;/span&gt; 825 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-sha256&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;openssl x509 &lt;span class=&quot;nt&quot;&gt;-req&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-in&lt;/span&gt; client.csr &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-CA&lt;/span&gt; ca.pem &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-CAkey&lt;/span&gt; ca.key &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-CAcreateserial&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; client.pem &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-days&lt;/span&gt; 825 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-sha256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;server.key&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;server.pem&lt;/code&gt; files are installed on the server, and the &lt;code class=&quot;highlighter-rouge&quot;&gt;client.key&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;client.pem&lt;/code&gt; files are presented by the client when creating the connection. This ensures authentication happens in both directions, the client authenticates the server, and the server authenticates the client.&lt;/p&gt;

&lt;p&gt;The root certificate (&lt;code class=&quot;highlighter-rouge&quot;&gt;ca.pem&lt;/code&gt;) serves as the trusted authority that vouches for the client and server certificates. That’s the certificate we implicitly trust, so store it safely!&lt;/p&gt;

&lt;p&gt;If an attacker attempts to connect with their own certificates, authentication will fail as the server’s certificate won’t have been signed by the CA certificate presented by the attacker.&lt;/p&gt;

&lt;p&gt;Let’s look at how to set this up in PostgreSQL.&lt;/p&gt;

&lt;h2 id=&quot;securing-postgresql-with-self-signed-certificates&quot;&gt;Securing PostgreSQL with self-signed certificates&lt;/h2&gt;

&lt;p&gt;This section assumes you have PostgreSQL installed on your server.&lt;/p&gt;

&lt;p&gt;Allow the PostgreSQL TCP port through your firewall:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;ufw allow 5432/tcp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ensure PostgreSQL binds to all available TCP interfaces:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# /etc/postgresql/&amp;lt;version&amp;gt;/main/postgresql.conf

# ...

listen_addresses = &apos;*&apos;

# ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;aside&gt;
  &lt;p&gt;You can get the exact path to the PostgreSQL config file by running &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo -u postgres psql -c &quot;SHOW config_file;&quot;&lt;/code&gt;&lt;/p&gt;&lt;/aside&gt;

&lt;p&gt;Force SSL authentication on the server for external connections:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# /etc/postgresql/&amp;lt;version&amp;gt;/main/pg_hba.conf

# TYPE  DATABASE    USER      ADDRESS     METHOD
# Enforce SSL authentication for all client connections
hostssl all         all       0.0.0.0/0   trust     clientcert=verify-ca
# Reject all non SSL connections
host    all         all       0.0.0.0/0   reject
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Setting &lt;code class=&quot;highlighter-rouge&quot;&gt;clientcert&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;verify-ca&lt;/code&gt; verifies the client certificate against the trusted root certificate.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;There is stricter setting for &lt;code class=&quot;highlighter-rouge&quot;&gt;clientcert&lt;/code&gt; which is &lt;code class=&quot;highlighter-rouge&quot;&gt;verify-full&lt;/code&gt;. This verifies the &lt;code class=&quot;highlighter-rouge&quot;&gt;CN&lt;/code&gt; metadata attribute of the certificate which must match the username or another applicable mapping. For my purposes, this is overkill so I use &lt;code class=&quot;highlighter-rouge&quot;&gt;verify-ca&lt;/code&gt;.&lt;/p&gt;&lt;/aside&gt;

&lt;p&gt;Upload the root certificate (&lt;code class=&quot;highlighter-rouge&quot;&gt;ca.pem&lt;/code&gt;), server private key (&lt;code class=&quot;highlighter-rouge&quot;&gt;server.key&lt;/code&gt;), and server certificate (&lt;code class=&quot;highlighter-rouge&quot;&gt;server.pem&lt;/code&gt;) to the server using SCP.&lt;/p&gt;

&lt;p&gt;Ensure all 3 files are owned by the &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres&lt;/code&gt; user:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chown &lt;/span&gt;postgres:postgres path/to/ca.pem
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chown &lt;/span&gt;postgres:postgres path/to/server.key
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chown &lt;/span&gt;postgres:postgres path/to/server.pem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The private key must have &lt;code class=&quot;highlighter-rouge&quot;&gt;600&lt;/code&gt; permissions:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chmod &lt;/span&gt;600 path/to/server.key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Open the PostgreSQL configuration once again and add in the paths to these files:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# /etc/postgresql/&amp;lt;version&amp;gt;/main/postgresql.conf

ssl = on
ssl_ca_file = &apos;/path/to/ca.pem&apos;
ssl_cert_file = &apos;/path/to/server.pem&apos;
ssl_key_file = &apos;/path/to/server.key&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restart the PostgreSQL server:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl restart postgresql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can now connect to the PostgreSQL server using:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;psql &lt;span class=&quot;s2&quot;&gt;&quot;host=&amp;lt;server_ip_address&amp;gt; user=postgres sslcert=./client.pem sslkey=./client.key sslrootcert=./ca.pem sslmode=verify-ca&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;sslmode=verify-ca&lt;/code&gt; on the client connection ensures authentication is performed in both directions. Using this technique, connections to PostgreSQL are secure over the internet.&lt;/p&gt;

&lt;h2 id=&quot;securing-redis-with-self-signed-certificates&quot;&gt;Securing Redis with self-signed certificates&lt;/h2&gt;

&lt;p&gt;This section assumes you have Redis installed on your server.&lt;/p&gt;

&lt;p&gt;Allow the Redis TCP port through your firewall:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;ufw allow 6379/tcp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Upload the root certificate (&lt;code class=&quot;highlighter-rouge&quot;&gt;ca.pem&lt;/code&gt;), server private key (&lt;code class=&quot;highlighter-rouge&quot;&gt;server.key&lt;/code&gt;), and server certificate (&lt;code class=&quot;highlighter-rouge&quot;&gt;server.pem&lt;/code&gt;) to the server using SCP.&lt;/p&gt;

&lt;p&gt;Ensure the files are owned by the &lt;code class=&quot;highlighter-rouge&quot;&gt;redis&lt;/code&gt; user:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chown &lt;/span&gt;redis:redis path/to/ca.pem
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chown &lt;/span&gt;redis:redis path/to/server.key
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chown &lt;/span&gt;redis:redis path/to/server.pem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The private key must have &lt;code class=&quot;highlighter-rouge&quot;&gt;600&lt;/code&gt; permissions:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chmod &lt;/span&gt;600 path/to/server.key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Open the Redis configuration file and make the following changes:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# /etc/redis/redis.conf

bind * -::*

# Protected mode requires a password for external connections,
# but since we&apos;re using SSL certs for authentication, we can safely
# disable it.
protected-mode no

port 0
tls-port 6379

tls-cert-file path/to/server.pem
tls-key-file path/to/server.key
tls-ca-cert-file path/to/ca.pem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restart Redis:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl restart redis
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Redis will now perform SSL authentication in both directions. Connect to the server by presenting the client certificate using:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;redis-cli &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; &amp;lt;server_ip&amp;gt; &lt;span class=&quot;nt&quot;&gt;--cert&lt;/span&gt; ./client.pem &lt;span class=&quot;nt&quot;&gt;--key&lt;/span&gt; ./client.key &lt;span class=&quot;nt&quot;&gt;--cacert&lt;/span&gt; ./ca.pem &lt;span class=&quot;nt&quot;&gt;--tls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;In this post we covered how to issue a root certificate and sign client and server certificates using it. We then secured connections to PostgreSQL and Redis using these certificates.&lt;/p&gt;

&lt;p&gt;We haven’t covered security best practices to keep the private keys and certificates safe. These are largely organisation dependent and out of scope for a simple guide like this one.&lt;/p&gt;

&lt;p&gt;Ensure you keep your private keys extremely safe, especially the root CA’s private key. Issue a new certificate for each service, and in some cases you may wish to issue one for each user of each service.&lt;/p&gt;

&lt;p&gt;The docs of web services such as Redis and PostgreSQL are very thorough in their explanation of TLS configuration, so consult those to formulate your deployment strategy, and keep your services secure!&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/ruby-installation-and-management-with-rv&quot;&gt;Ruby version management with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/postgresql-server-setup-for-rails-apps&quot;&gt;PostgreSQL server setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/redis-server-setup-for-rails-apps&quot;&gt;Redis setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/deploying-rails-apps-to-a-server-using-mimas&quot;&gt;Deploying Rails apps using Mimas&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">Anatomy of a Ruby gem</title><link href="https://binarysolo.blog/anatomy-of-a-ruby-gem/" rel="alternate" type="text/html" title="Anatomy of a Ruby gem" /><published>2026-09-02T00:00:00+01:00</published><updated>2026-09-02T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-09-02-anatomy-of-a-ruby-gem.md</id><content type="html" xml:base="https://binarysolo.blog/anatomy-of-a-ruby-gem/">&lt;p&gt;A Ruby gem is a package of Ruby code. It’s usually uploaded to a gem server such as &lt;a href=&quot;https://rubygems.org&quot;&gt;RubyGems&lt;/a&gt;, although a newer alternative called &lt;a href=&quot;https://gem.coop&quot;&gt;gem.coop&lt;/a&gt; is emerging.&lt;/p&gt;

&lt;p&gt;A gem is usually installed using &lt;a href=&quot;https://bundler.io&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;bundler&lt;/code&gt;&lt;/a&gt;, which is Ruby’s official package and dependency manager.&lt;/p&gt;

&lt;p&gt;While most Rails developers may never need to build a gem from scratch, I always learn loads when working with plain Ruby outside of Rails.&lt;/p&gt;

&lt;p&gt;A Ruby gem doesn’t have a rigid directory structure like Rails, nor are tests automatically set up. This is all left up to the developer. In this post, I’ll cover how to create a Ruby gem from scratch, including setting up Minitest for the test suite.&lt;/p&gt;

&lt;h2 id=&quot;structure-of-a-gem&quot;&gt;Structure of a gem&lt;/h2&gt;

&lt;p&gt;Create a folder for your new gem (we’ll use the placeholder name &lt;em&gt;my-gem&lt;/em&gt; throughout this tutorial):&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;my-gem
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;my-gem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, create a &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;gemspec&lt;/code&gt; for it, as well as a &lt;code class=&quot;highlighter-rouge&quot;&gt;License&lt;/code&gt; file:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;Gemfile
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;my-gem.gemspec
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;LICENSE.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; contains the dependencies required to locally develop the gem, such as &lt;code class=&quot;highlighter-rouge&quot;&gt;debug&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;rake&lt;/code&gt;, or &lt;code class=&quot;highlighter-rouge&quot;&gt;minitest&lt;/code&gt;. Dependencies required for your gem’s functionality must be defined in the &lt;em&gt;gemspec&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;my-gem.gemspec&lt;/code&gt; file contains all metadata regarding your gem, including it’s runtime dependencies.&lt;/p&gt;

&lt;p&gt;Add your chosen open source license to the &lt;code class=&quot;highlighter-rouge&quot;&gt;LICENSE.txt&lt;/code&gt; file. I usually release everything I write under the &lt;a href=&quot;https://en.wikipedia.org/wiki/MIT_License&quot;&gt;MIT license&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create a basic folder structure to contain your code:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;lib
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir test&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;bin
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;scripts
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;lib&lt;/code&gt; folder contains your package’s code. The &lt;code class=&quot;highlighter-rouge&quot;&gt;test&lt;/code&gt; folder contains your test suite. &lt;code class=&quot;highlighter-rouge&quot;&gt;bin&lt;/code&gt; is for any executables your package may emit, and the &lt;code class=&quot;highlighter-rouge&quot;&gt;scripts&lt;/code&gt; folder is for development scripts such as linting, running tests, or releasing the gem.&lt;/p&gt;

&lt;h3 id=&quot;directory-skeleton&quot;&gt;Directory skeleton&lt;/h3&gt;

&lt;p&gt;The folder structure is pretty free-form, but this is the logical structure I like to use. You may wish to use a different structure if you prefer — the structure isn’t enforced.&lt;/p&gt;

&lt;p&gt;Within these folders, pretty much anything goes, but there are a few conventions worth using.&lt;/p&gt;

&lt;p&gt;Under &lt;code class=&quot;highlighter-rouge&quot;&gt;lib&lt;/code&gt;, there should be a file named after your gem which is the entry point for your package, and a folder also named after your gem containing the implementation.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;lib
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;my-gem
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;my-gem.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a version file with a constant denoting the gem’s version number:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;my-gem/version.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# lib/my-gem/version.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;MyGem&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;VERSION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;0.1.0&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;the-gemspec&quot;&gt;The gemspec&lt;/h3&gt;

&lt;p&gt;With these files in place, we can implement a basic gemspec:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# my-gem.gemspec&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Read in the version from the file we just created&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require_relative&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;lib/my-gem/version&quot;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Gem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Specification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my-gem&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;version&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;MyGem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;VERSION&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;author&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Enter your name&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Enter your email address&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;summary&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Enter a brief summary for what your gem does&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;license&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MIT&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;homepage&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Add a link to your gem&apos;s homepage. Usually the repo&apos;s URL.&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# The files to ship with your gem. I like to ship the fewest files required&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# for the gem to work, hence git is used to read all files under `lib`, `bin`&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# and the LICENSE file.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;files&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`git ls-files -z`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;%r!^(lib|bin)/|^LICENSE!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Any executable files provided by your gem&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executables&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-gem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# The location of the executable files&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bindir&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bin&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# The paths to add to Ruby&apos;s `LOAD_PATH` when your gem is activated.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;require_paths&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lib&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# The minimum Ruby version&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;required_ruby_version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;= 3.4.0&quot;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Runtime dependencies for your gem&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_dependency&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dry-cli&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;        &lt;span class=&quot;s2&quot;&gt;&quot;~&amp;gt; 1.4&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The full specification for this file can be found here: &lt;a href=&quot;https://guides.rubygems.org/specification-reference/&quot;&gt;https://guides.rubygems.org/specification-reference/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read the gemspec in your Gemfile so dependencies are loaded:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Gemfile&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://rubygems.org&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;gemspec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;writing-code-for-the-package&quot;&gt;Writing code for the package&lt;/h2&gt;

&lt;p&gt;The implementation of your package goes in the &lt;code class=&quot;highlighter-rouge&quot;&gt;lib/my-gem&lt;/code&gt; folder. The file structure is completely up to you, and it will depend on what you’re building.&lt;/p&gt;

&lt;p&gt;All files need to be manually &lt;code class=&quot;highlighter-rouge&quot;&gt;require&lt;/code&gt;d, or &lt;code class=&quot;highlighter-rouge&quot;&gt;autoload&lt;/code&gt;ed in &lt;code class=&quot;highlighter-rouge&quot;&gt;lib/my-gem.rb&lt;/code&gt;. You may use &lt;a href=&quot;https://github.com/fxn/zeitwerk&quot;&gt;Zeitwerk&lt;/a&gt; to autoload your code, as this is what Rails uses, but I personally don’t use it in gems as I prefer more control over my file structure and module names.&lt;/p&gt;

&lt;p&gt;Let’s create a basic implementation file to continue with this demonstration:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;lib/my-gem/hello.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# lib/my-gem/hello.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;MyGem&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Hello&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;say&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Hello world!&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# lib/my-gem.rb&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require_relative&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my-gem/hello&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ensure all your code is namespaced by a module named after your gem to prevent conflicts.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-the-test-suite&quot;&gt;Setting up the test suite&lt;/h2&gt;

&lt;p&gt;Next, let’s set up Minitest to run a test suite. Add minitest to your Gemfile:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Gemfile&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://rubygems.org&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;gemspec&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rake&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:test&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest-profile&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest-reporters&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We add &lt;code class=&quot;highlighter-rouge&quot;&gt;rake&lt;/code&gt; to run the test suite, and a couple of other minitest packages to improve the testing experience.&lt;/p&gt;

&lt;p&gt;All test file names must conventionally start with &lt;code class=&quot;highlighter-rouge&quot;&gt;test_&lt;/code&gt;. Create &lt;code class=&quot;highlighter-rouge&quot;&gt;test/helper.rb&lt;/code&gt; to contain the global test configuration:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch test&lt;/span&gt;/helper.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# test/helper.rb&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest/autorun&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest/reporters&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Require your gem&apos;s code&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;expand_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;../lib/my-gem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__dir__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Customise the test reporter&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Minitest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Reporters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;use!&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Minitest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Reporters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;DefaultReporter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;color: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;MyGem&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# The base class for your tests.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestCase&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Minitest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Test&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;DSL&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# A basic DSL method so tests can be declared using `test &quot;&quot; do ... end`&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# just like Rails.&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;test_&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;gsub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/\s+/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;_&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_sym&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Duplicate &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; in &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;method_defined?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;block_given?&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;define_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;define_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Please implement the test: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;DSL&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We do some basic setup in the helper as explain in the comments. The most important part is the &lt;code class=&quot;highlighter-rouge&quot;&gt;test&lt;/code&gt; DSL method. Without this, test methods would have to be defined as&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_does_something&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;which is not very readable. Using this DSL, we can write:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;does something&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a test:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch test&lt;/span&gt;/test_hello.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# test/test_hello.rb&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;helper&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;MyGem&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestHello&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;TestCase&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;says hello&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Capture STDOUT so we can read what&apos;s written using `puts`&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;original_stdout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vg&quot;&gt;$stdout&lt;/span&gt;
      &lt;span class=&quot;vg&quot;&gt;$stdout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;StringIO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;

      &lt;span class=&quot;no&quot;&gt;Hello&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;say&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;assert_equal&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Hello world!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vg&quot;&gt;$stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;ensure&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Ensure we reset `STDOUT` back to what it was&lt;/span&gt;
      &lt;span class=&quot;vg&quot;&gt;$stdout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;original_stdout&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a Rakefile and initialise the test task provided by Minitest so we can run the tests:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;Rakefile
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Rakefile&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest/test_task&quot;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Minitest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TestTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Install dependencies and then run the tests:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;rake &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can run specific tests using the &lt;code class=&quot;highlighter-rouge&quot;&gt;I&lt;/code&gt; argument:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;rake &lt;span class=&quot;nb&quot;&gt;test &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/hello/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;More details on the Rake task and how to run tests using Minitest is available &lt;a href=&quot;https://minite.st/docs/#running-your-tests&quot;&gt;in the official docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now that we have the tests working, let’s look at releasing the gem.&lt;/p&gt;

&lt;h2 id=&quot;building-and-releasing-the-gem&quot;&gt;Building and releasing the gem&lt;/h2&gt;

&lt;p&gt;To release the gem, we first need to build it. In your gemspec, comment out the following lines:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# spec.executables   = [&quot;my-gem&quot;]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# spec.bindir        = &quot;bin&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We haven’t created any executables, so this will result in an invalid spec. Then, initialise a git repo and commit your code:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git init
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git add &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Initial commit ...&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And finally, build your gem:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;gem build my-gem.gemspec
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will create a &lt;code class=&quot;highlighter-rouge&quot;&gt;.gem&lt;/code&gt; file in your current directory. This is a compressed ZIP file. Rename the extension to &lt;code class=&quot;highlighter-rouge&quot;&gt;.zip&lt;/code&gt; and you can extract it using your OS’s archiving utility.&lt;/p&gt;

&lt;p&gt;Within this archive, you’ll find three more archives:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;checksums.yaml.gz&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;data.tar.gz&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;metadata.gz&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;checksums.yaml.gz&lt;/code&gt; contains signatures for your package to ensure its integrity.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;data.tar.gz&lt;/code&gt; contains your package’s code.&lt;/p&gt;

&lt;p&gt;And, &lt;code class=&quot;highlighter-rouge&quot;&gt;metadata.gz&lt;/code&gt; is your evaluated gemspec, stored as a YAML dump of a &lt;code class=&quot;highlighter-rouge&quot;&gt;Gem::Specification&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;The entire &lt;code class=&quot;highlighter-rouge&quot;&gt;.gem&lt;/code&gt; archive is pushed to a Gem server for distribution.&lt;/p&gt;

&lt;h2 id=&quot;distributing-your-gem&quot;&gt;Distributing your gem&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://rubygems.org&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;RubyGems.org&lt;/code&gt;&lt;/a&gt; is the most popular open source gem server and the default for most projects. Create an account on their website.&lt;/p&gt;

&lt;p&gt;Then, add the rake task to release your gem to your &lt;code class=&quot;highlighter-rouge&quot;&gt;Rakefile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Rakefile&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;bundler/gem_tasks&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minitest/test_task&quot;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Minitest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TestTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Release your gem using:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;rake release
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You may have to log into RubyGems on your terminal. Follow the on-screen instructions. After pushing your gem, users can add it to their Gemfile to use it in their projects!&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;

&lt;p&gt;That was a look inside a basic Ruby gem. There’s always a lot to learn when your remove all the supports Rails gives you, and building a Ruby gem is a great way to level up your pure Ruby skills.&lt;/p&gt;

&lt;p&gt;To improve your experience, you can add scripts to your &lt;code class=&quot;highlighter-rouge&quot;&gt;scripts/&lt;/code&gt; folder to run your tests and release your gem.&lt;/p&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">Deploying Rails apps to a server using Mimas</title><link href="https://binarysolo.blog/deploying-rails-apps-to-a-server-using-mimas/" rel="alternate" type="text/html" title="Deploying Rails apps to a server using Mimas" /><published>2026-09-01T00:00:00+01:00</published><updated>2026-09-01T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-09-01-deploying-rails-apps-using-mimas.md</id><content type="html" xml:base="https://binarysolo.blog/deploying-rails-apps-to-a-server-using-mimas/">&lt;p&gt;Deploying a Rails app to a server can have many moving parts. We need a reverse proxy to terminate SSL and serve static assets, ensure deploy errors don’t affect the active release and that successful deploys are zero-downtime, and finally, configure log rotation and database backups.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://codeberg.org/ayushn21/mimas&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;mimas&lt;/code&gt;&lt;/a&gt;, along with its &lt;a href=&quot;https://codeberg.org/ayushn21/mimas-rails&quot;&gt;Rails plugin&lt;/a&gt; automates all of this, making it easy to self-host your Rails apps instead of paying the ridiculous prices for a PaaS.&lt;/p&gt;

&lt;p&gt;See my &lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;blog series on Ruby app deployment&lt;/a&gt; to understand how Mimas works under the hood.&lt;/p&gt;

&lt;h2 id=&quot;adding-mimas-to-your-app&quot;&gt;Adding Mimas to your app&lt;/h2&gt;

&lt;p&gt;Add Mimas and its Rails plugin to your bundle and create a binstub for it:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle add mimas-rails &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; development
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle binstub mimas
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Run the &lt;code class=&quot;highlighter-rouge&quot;&gt;init&lt;/code&gt; command to setup the config files needed by Mimas:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bin/mimas init
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You’ll be prompted for details about your domain and server. A pre-deploy hook to build your Rails assets will also be set up.&lt;/p&gt;

&lt;p&gt;A &lt;code class=&quot;highlighter-rouge&quot;&gt;/deploy&lt;/code&gt; folder will be created with a &lt;code class=&quot;highlighter-rouge&quot;&gt;mimas.rb&lt;/code&gt; configuration file, a &lt;code class=&quot;highlighter-rouge&quot;&gt;puma.rb&lt;/code&gt; file to configure Puma in production, and a &lt;code class=&quot;highlighter-rouge&quot;&gt;Caddyfile&lt;/code&gt; to configure the Caddy reverse proxy.&lt;/p&gt;

&lt;p&gt;All these files are self-documented. Consult the &lt;a href=&quot;https://puma.io&quot;&gt;Puma&lt;/a&gt; and &lt;a href=&quot;https://caddyserver.com&quot;&gt;Caddy&lt;/a&gt; docs for further information on those files.&lt;/p&gt;

&lt;h2 id=&quot;deploying-your-app&quot;&gt;Deploying your app&lt;/h2&gt;

&lt;p&gt;Provision your production server if you haven’t already:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bin/mimas provision production
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you need PostgreSQL or Redis, use the &lt;a href=&quot;https://codeberg.org/ayushn21/mimas-postgresql&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;mimas-postgresql&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://codeberg.org/ayushn21/mimas-redis&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;mimas-redis&lt;/code&gt;&lt;/a&gt; plugins to set those up.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bin/mimas postgresql setup production
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bin/mimas redis setup production
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a database if you’re not using SQLite:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bin/mimas postgresql create_db my_app_production production
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Make a note of the database password generated.&lt;/p&gt;

&lt;p&gt;Create an &lt;code class=&quot;highlighter-rouge&quot;&gt;.env&lt;/code&gt; file on the server for your app’s sensitive information:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bin/mimas &lt;span class=&quot;nb&quot;&gt;env &lt;/span&gt;edit production
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will open your text editor. Add in environment variables as key-value pairs:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;RAILS_MASTER_KEY=...
DATABASE_PASSWORD=...
REDIS_URL=unix:///run/redis/redis-server.sock
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You don’t need to set &lt;code class=&quot;highlighter-rouge&quot;&gt;RAILS_ENV&lt;/code&gt; as that is set in the Mimas configuration (&lt;code class=&quot;highlighter-rouge&quot;&gt;mimas.rb&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Ensure you update your &lt;code class=&quot;highlighter-rouge&quot;&gt;database.yml&lt;/code&gt; to read the &lt;code class=&quot;highlighter-rouge&quot;&gt;DATABASE_PASSWORD&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/database.yml&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my_app_production&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_PASSWORD&quot;] %&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my_app_production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Push your site to your server:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bin/mimas push production
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it! Your app should be live at the domain name you provided in &lt;code class=&quot;highlighter-rouge&quot;&gt;mimas.rb&lt;/code&gt;. If you run into any issues, ensure your DNS is configured correctly.&lt;/p&gt;

&lt;p&gt;All future deploys will be zero downtime. If a deploy throws an error, it will be rolled back and the existing service will be unaffected.&lt;/p&gt;

&lt;p&gt;To manually rollback a deployment, revert your code change and deploy again.&lt;/p&gt;

&lt;p&gt;Mimas is still in early stages of development, but I use it to run &lt;a href=&quot;https://scattergun.email&quot;&gt;Scattergun&lt;/a&gt;, so it’s fairly production tested. Give it a go and let file an issue (or better, create a PR), if you run into problems!&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/ruby-installation-and-management-with-rv&quot;&gt;Ruby version management with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/postgresql-server-setup-for-rails-apps&quot;&gt;PostgreSQL server setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/redis-server-setup-for-rails-apps&quot;&gt;Redis setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/generating-self-signed-certificates-for-web-services&quot;&gt;Generating self-signed certificates for web services&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">Redis server setup for Rails apps</title><link href="https://binarysolo.blog/redis-server-setup-for-rails-apps/" rel="alternate" type="text/html" title="Redis server setup for Rails apps" /><published>2026-08-26T00:00:00+01:00</published><updated>2026-08-26T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-08-26-redis-setup-for-rails-apps.md</id><content type="html" xml:base="https://binarysolo.blog/redis-server-setup-for-rails-apps/">&lt;p&gt;In this post, we’ll discuss how to install and configure Redis on an Ubuntu server for use with Rails apps.&lt;/p&gt;

&lt;h2 id=&quot;installing-redis&quot;&gt;Installing Redis&lt;/h2&gt;

&lt;p&gt;Install Redis using the &lt;code class=&quot;highlighter-rouge&quot;&gt;apt&lt;/code&gt; utility:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;redis-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will create a new Linux user called &lt;code class=&quot;highlighter-rouge&quot;&gt;redis&lt;/code&gt;, and install a systemd service to run Redis under that user.&lt;/p&gt;

&lt;h2 id=&quot;configuring-redis&quot;&gt;Configuring Redis&lt;/h2&gt;

&lt;p&gt;Redis has a configuration file located at &lt;code class=&quot;highlighter-rouge&quot;&gt;/etc/redis/redis.conf&lt;/code&gt;. The file is largely self-documented, so I recommend reading through it to see the avaiable options. Depending on what you’re using Redis for, you may wish to set the &lt;code class=&quot;highlighter-rouge&quot;&gt;maxmemory&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;maxmemory-policy&lt;/code&gt; settings.&lt;/p&gt;

&lt;p&gt;For example, a cache server might set an expiration policy for &lt;code class=&quot;highlighter-rouge&quot;&gt;maxmemory-policy&lt;/code&gt;, but when used as Sidekiq’s data-store, you absolutely do not want it to drop jobs when &lt;code class=&quot;highlighter-rouge&quot;&gt;maxmemory&lt;/code&gt; is hit, so a &lt;code class=&quot;highlighter-rouge&quot;&gt;noeviction&lt;/code&gt; policy is appropriate.&lt;/p&gt;

&lt;p&gt;When running Redis on the same server as your Rails app, make the following changes to the config file:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;bind 0.0.0.0
port 0

unixsocket /run/redis/redis-server.sock
unixsocketperm 777
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;bind&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;port&lt;/code&gt; directives disable TCP binding, so Redis is only available via a Unix socket. This keeps it secure as the server cannot be accessed from outside the machine.&lt;/p&gt;

&lt;p&gt;Then we define the path to the Unix socket using &lt;code class=&quot;highlighter-rouge&quot;&gt;unixsocket&lt;/code&gt;, and the file permissions for the socket file using &lt;code class=&quot;highlighter-rouge&quot;&gt;unixsocketperm&lt;/code&gt;. &lt;code class=&quot;highlighter-rouge&quot;&gt;777&lt;/code&gt; means all users on the machine have execute permissions on the socket file, and hence can access Redis via a Unix socket.&lt;/p&gt;

&lt;p&gt;I recommend against exposing Redis to the internet. If you run Redis on a standalone server, create a private IP for it and only bind it to that IP address. If you must expose it to the internet, ensure you create a self-signed certificate to authenticate and encrypt access. That setup is out of scope for this guide.&lt;/p&gt;

&lt;p&gt;Restart the Redis service to apply the changes:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl restart redis-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can now supply the Redis URL to the Rails app:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;unix:///run/redis/redis-server.sock
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I recommend setting this value in an environment variable and reading it into your app where required.&lt;/p&gt;

&lt;h2 id=&quot;redis-and-mimas&quot;&gt;Redis and Mimas&lt;/h2&gt;

&lt;p&gt;The &lt;a href=&quot;https://codeberg.org/ayushn21/mimas&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;mimas&lt;/code&gt;&lt;/a&gt; deployment tool for Ruby apps has a &lt;a href=&quot;https://codeberg.org/ayushn21/mimas-redis&quot;&gt;Redis plugin&lt;/a&gt; to automate this process.&lt;/p&gt;

&lt;p&gt;Everything covered in this article is automated by the plugin. Have a look at the source code to dig deeper!&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/ruby-installation-and-management-with-rv&quot;&gt;Ruby version management with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/postgresql-server-setup-for-rails-apps&quot;&gt;PostgreSQL server setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/deploying-rails-apps-to-a-server-using-mimas&quot;&gt;Deploying Rails apps using Mimas&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/generating-self-signed-certificates-for-web-services&quot;&gt;Generating self-signed certificates for web services&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">PostgreSQL server setup for Rails apps</title><link href="https://binarysolo.blog/postgresql-server-setup-for-rails-apps/" rel="alternate" type="text/html" title="PostgreSQL server setup for Rails apps" /><published>2026-08-25T00:00:00+01:00</published><updated>2026-08-25T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-08-25-postgresql-server-setup-for-rails-apps.md</id><content type="html" xml:base="https://binarysolo.blog/postgresql-server-setup-for-rails-apps/">&lt;p&gt;Following on from my series on &lt;a href=&quot;https://binarysolo.blog/server-setup-for-ruby-web-applications/&quot;&gt;Ruby app deployment&lt;/a&gt;, in this post, I’ll cover the installation and setup of PostgreSQL for Ruby and Rails apps.&lt;/p&gt;

&lt;p&gt;We’ll discuss how to install, secure, and backup PostgreSQL databases on an Ubuntu server.&lt;/p&gt;

&lt;h2 id=&quot;installing-postgresql&quot;&gt;Installing PostgreSQL&lt;/h2&gt;

&lt;p&gt;Install PostgreSQL using &lt;code class=&quot;highlighter-rouge&quot;&gt;apt&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;postgresql postgresql-contrib
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;postgresql-contrib&lt;/code&gt; package installs a number of useful extensions. The full list can be found &lt;a href=&quot;https://github.com/postgres/postgres/tree/master/contrib&quot;&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The above command will create a new Linux user called &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres&lt;/code&gt; and install a systemd service to run PostgreSQL under this user.&lt;/p&gt;

&lt;p&gt;You can ensure the server is running by accessing the CLI:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres psql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;configuring-postgresql&quot;&gt;Configuring PostgreSQL&lt;/h2&gt;

&lt;p&gt;PostgreSQL has its own concept of &lt;em&gt;users&lt;/em&gt; (also known as &lt;em&gt;roles&lt;/em&gt;), which are useful for access control to different databases. The default role is called &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres&lt;/code&gt; and is a super user with full-access to all databases on the server. I recommend against using this role to access databases from your Rails app. Create a new role for each app (we’ll discuss this shortly).&lt;/p&gt;

&lt;h3 id=&quot;the-configuration-files&quot;&gt;The configuration files&lt;/h3&gt;

&lt;p&gt;PostgreSQL has two main configuration files that we are concerned with: &lt;code class=&quot;highlighter-rouge&quot;&gt;postgresql.conf&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;pg_hba.conf&lt;/code&gt;. Both these files are located in the config directory, which can be found by running:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres psql &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;SHOW config_file;&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;postgresql.conf&lt;/code&gt; contains settings related to the running of the server, such as memory settings, and the location of the data directory. The file is self-documented so you can have a read through it to understand the options.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;pg_hba.conf&lt;/code&gt; defines access control to the server and its databases. They are defined in a table-like format as:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             postgres                                peer

# ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The default rules will vary depending on your installation of PostgreSQL, but it will always contain the above rule which allows the &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres&lt;/code&gt; Linux user access to the whole database server. The value &lt;code class=&quot;highlighter-rouge&quot;&gt;peer&lt;/code&gt; under &lt;code class=&quot;highlighter-rouge&quot;&gt;METHOD&lt;/code&gt; means that the &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres&lt;/code&gt; Linux user can access the &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres&lt;/code&gt; role within PostgreSQL without additional authentication.&lt;/p&gt;

&lt;p&gt;I recommend the following additional rules in this file:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# TYPE  DATABASE        USER            ADDRESS                 METHOD
# Allow access via the local Unix socket to all databases and roles using password authentication
local   all             all                                     scram-sha-256

# Allow access via `localhost` to all databases and roles using password authentication
host    all             all             127.0.0.1/32            scram-sha-256
host    all             all             ::1/128                 scram-sha-256

# Reject all external connections
host    all             all             0.0.0.0/0               reject
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can find the &lt;a href=&quot;https://www.postgresql.org/docs/current/auth-pg-hba-conf.html&quot;&gt;complete documentation for this file in the PostgreSQL docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When running your Rails app on a single server, where the app and database are on the same machine, it’s best to access the PostgreSQL server via the Unix socket.&lt;/p&gt;

&lt;p&gt;In such a setup, you can still access your database using SSH. GUI apps like &lt;a href=&quot;https://dbeaver.com&quot;&gt;DBeaver&lt;/a&gt; can also be used through an SSH tunnel.&lt;/p&gt;

&lt;p&gt;If you must expose PostgreSQL to the internet, ensure you issue self-signed certificates to authenticate access. Setting this up is out of scope for this guide.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;If you run the database on its own server, never expose it to the internet. Set up a private network on your cloud provider, and only allow access to PostgreSQL on the private IP address.&lt;/p&gt;

&lt;p&gt;You can configure this in your firewall and the &lt;code class=&quot;highlighter-rouge&quot;&gt;postgresql.conf&lt;/code&gt; file.&lt;/p&gt;&lt;/aside&gt;

&lt;h2 id=&quot;creating-a-role-and-database&quot;&gt;Creating a role and database&lt;/h2&gt;

&lt;p&gt;After PostgreSQL has been configured, and access control defined, we can create a role and database for our Rails app.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres psql &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;CREATE ROLE &amp;lt;app_name&amp;gt; WITH LOGIN PASSWORD &amp;lt;password&amp;gt;&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ensure you generate a secure password for the role using a password manager, or use openssl on the command line as:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;openssl rand &lt;span class=&quot;nt&quot;&gt;-hex&lt;/span&gt; 64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once you have created the role, create all the databases your app needs:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres psql &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;CREATE DATABASE &amp;lt;app_name&amp;gt;_production OWNER &amp;lt;app_name&amp;gt;&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres psql &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;CREATE DATABASE &amp;lt;app_name&amp;gt;_production_queue OWNER &amp;lt;app_name&amp;gt;&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres psql &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;CREATE DATABASE &amp;lt;app_name&amp;gt;_production_cache OWNER &amp;lt;app_name&amp;gt;&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres psql &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;CREATE DATABASE &amp;lt;app_name&amp;gt;_production_cable OWNER &amp;lt;app_name&amp;gt;&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;All these databases can now be accessed using the role name and password based on our earlier &lt;code class=&quot;highlighter-rouge&quot;&gt;pg_hba.conf&lt;/code&gt; settings. Using this technique rather than &lt;code class=&quot;highlighter-rouge&quot;&gt;peer&lt;/code&gt; authentication, means we don’t have to create a new Linux user for each app.&lt;/p&gt;

&lt;p&gt;Configure Rails’s &lt;code class=&quot;highlighter-rouge&quot;&gt;database.yml&lt;/code&gt; to point to these databases:&lt;/p&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;primary&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_USER&quot;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_PASSWORD&quot;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;app_name&amp;gt;_production&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_USER&quot;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_PASSWORD&quot;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;app_name&amp;gt;_production_cache&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;migrations_paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;db/cache_migrate&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_USER&quot;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_PASSWORD&quot;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;app_name&amp;gt;_production_queue&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;migrations_paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;db/queue_migrate&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;cable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_USER&quot;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&quot;DATABASE_PASSWORD&quot;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;app_name&amp;gt;_production_cable&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;migrations_paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;db/cable_migrate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ensure you set the database user and password we created earlier as environment variables, and the above setup should &lt;em&gt;just work&lt;/em&gt;. In the absence of a database URL, the Ruby PostgreSQL client will automatically try and connect on the Unix socket.&lt;/p&gt;

&lt;h2 id=&quot;database-backups&quot;&gt;Database backups&lt;/h2&gt;

&lt;p&gt;Database backups are absolutely critical. It’s arguably more important than the application code, as no product or company can recover from losing the database.&lt;/p&gt;

&lt;p&gt;The frequency of backups will depend on your application’s scale. For most small-scale apps, hourly backups should be sufficient.&lt;/p&gt;

&lt;p&gt;Never store backups on the same machine as your database server, as this defeats the purpose of the backups in the first place. If your database server is destroyed, you’ll lose the backups too.&lt;/p&gt;

&lt;p&gt;On Hetzner, I use their &lt;a href=&quot;https://www.hetzner.com/cloud/block-storage/&quot;&gt;block storage&lt;/a&gt; for database backups. These are cheap and can be mounted as volumes on your database server. They’re also replicated across three physical servers ensuring data integrity.&lt;/p&gt;

&lt;p&gt;For additional security, we’ll also encrypt our backups to keep user data safe.&lt;/p&gt;

&lt;h3 id=&quot;pg_dump&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;pg_dump&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;PostgreSQL has a utility called &lt;code class=&quot;highlighter-rouge&quot;&gt;pg_dump&lt;/code&gt; to create backups.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres pg_dump &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;custom &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;postgres &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--dbname&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;db_name&amp;gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; database_backup.dump
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;custom&lt;/code&gt; format is a compressed binary format which keeps the backup size efficient, and allows for easy restores. We use the &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres&lt;/code&gt; user as it has access to all database and simplifies access control.&lt;/p&gt;

&lt;p&gt;Consult the &lt;a href=&quot;https://www.postgresql.org/docs/current/app-pgdump.html&quot;&gt;docs&lt;/a&gt; for complete usage information.&lt;/p&gt;

&lt;p&gt;To encrypt this dump, we’ll use hybrid encryption. This combines asymmetric key and symmetric key encrytion.&lt;/p&gt;

&lt;p&gt;We’ll first generate a public key and private key. The public key is kept on the server to encrypt the backups, and the private key must be downloaded and kept safely in a password manager, as it will be needed to decrypt backups.&lt;/p&gt;

&lt;p&gt;We can’t use these keys directly for encryption, as this method can only encrypt data smaller than the key size itself. As such, we’ll generate a new symmetric encryption key to encrypt the data, and then encrypt that key using the public key of our key-pair, and store that within the data.&lt;/p&gt;

&lt;p&gt;To decrypt the backup, we’ll use the private key to decrypt the symmetric key used to encrypt the data, and then use that key to decrypt the data itself.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;openssl&lt;/code&gt; has a utility called &lt;code class=&quot;highlighter-rouge&quot;&gt;smime&lt;/code&gt; which makes this entire process seamless. All we need to do first is generate our key pair.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;openssl req &lt;span class=&quot;nt&quot;&gt;-x509&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-newkey&lt;/span&gt; rsa:4096 &lt;span class=&quot;nt&quot;&gt;-keyout&lt;/span&gt; private_key.pem &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; pg_dump_certificate.pem &lt;span class=&quot;nt&quot;&gt;-days&lt;/span&gt; 36500 &lt;span class=&quot;nt&quot;&gt;-nodes&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-subj&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;/CN=PostgreSQL Backup Certificate&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Download &lt;code class=&quot;highlighter-rouge&quot;&gt;private_key.pem&lt;/code&gt; from the server and keep it safe. Leave &lt;code class=&quot;highlighter-rouge&quot;&gt;pg_dump_certificate.pem&lt;/code&gt; on the server to encrypt backups.&lt;/p&gt;

&lt;p&gt;Backups can now be generated and encrypted using:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;pg_dump &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;custom &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;postgres &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--dbname&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&amp;lt;db_name&amp;gt; |
    openssl smime &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;-encrypt&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;-aes256&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;-binary&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;-outform&lt;/span&gt; DER &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; database_backup.dump.enc &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;path/to/certificate&amp;gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To decrypt the dump, run:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;openssl smime &lt;span class=&quot;nt&quot;&gt;-decrypt&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-binary&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-inform&lt;/span&gt; DER &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-in&lt;/span&gt; database_backup.dump.enc &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-inkey&lt;/span&gt; &amp;lt;path/to/private/key&amp;gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; backup.dump
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restore the dump using:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; postgres pg_restore &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; &amp;lt;role_name&amp;gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &amp;lt;database_name&amp;gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--no-owner&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-acl&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--clean&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &amp;lt;path/to/dump&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ensure the role and database exist on the server before attempting restoration.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;We covered a lot of details about PostgreSQL in this article. Running your own database instead of a managed database service can be a huge cost saving, but does come with additional work.&lt;/p&gt;

&lt;p&gt;That’s why I created &lt;a href=&quot;https://codeberg.org/ayushn21/mimas&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;mimas&lt;/code&gt;&lt;/a&gt; and the &lt;a href=&quot;https://codeberg.org/ayushn21/mimas-postgresql&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;mimas-postgresql&lt;/code&gt;&lt;/a&gt; plugin. These tools allow your to deploy Ruby and Rails apps to your own servers with ease. Everything discussed in this article has been automated by these tools.&lt;/p&gt;

&lt;p&gt;Have a look at the source code if you’re curious, and try them out on your projects!&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/ruby-installation-and-management-with-rv&quot;&gt;Ruby version management with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/redis-server-setup-for-rails-apps&quot;&gt;Redis setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/deploying-rails-apps-to-a-server-using-mimas&quot;&gt;Deploying Rails apps using Mimas&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/generating-self-signed-certificates-for-web-services&quot;&gt;Generating self-signed certificates for web services&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">Ruby installation and management with rv</title><link href="https://binarysolo.blog/ruby-installation-and-management-with-rv/" rel="alternate" type="text/html" title="Ruby installation and management with rv" /><published>2026-08-11T00:00:00+01:00</published><updated>2026-08-11T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-08-11-ruby-version-management-with-rv.md</id><content type="html" xml:base="https://binarysolo.blog/ruby-installation-and-management-with-rv/">&lt;p&gt;Ruby version management can be quite fiddly. All Ruby developers need multiple Ruby versions installed, as different projects may use different Rubies.&lt;/p&gt;

&lt;p&gt;For the past few years, I’ve used &lt;a href=&quot;https://github.com/rbenv/rbenv/&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt;&lt;/a&gt; along with &lt;a href=&quot;https://github.com/rbenv/ruby-build&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt;&lt;/a&gt; to install and manage Ruby versions. They’re good tools, but I was using them more out of inertia than anything else.&lt;/p&gt;

&lt;p&gt;When it came to building &lt;a href=&quot;https://codeberg.org/ayushn21/mimas&quot;&gt;Mimas&lt;/a&gt;, which is a tool to deploy Ruby apps to a VPS, I started researching alternatives. Initially, I landed on &lt;a href=&quot;https://github.com/postmodern/chruby&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://github.com/postmodern/ruby-install&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; simply manipulates environment variables to ensure the correct Ruby binary is selected, contrasting with &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt; which &lt;em&gt;shims&lt;/em&gt; the &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby&lt;/code&gt; command to intercept invokations and load the appropriate binary.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-build&lt;/code&gt; both make it easier to build Ruby binaries from source, but &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt; doesn’t need to be updated every time a new Ruby version is released.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt;’s cleaner approach of using environment variables rather than a shim was a huge selling point for me, but then I discovered &lt;a href=&quot;https://github.com/spinel-coop/rv/&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;what-is-rv&quot;&gt;What is &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;?&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/spinel-coop/rv/&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/a&gt; is a relatively new tool which aims to be a lot more than a Ruby version manager. See the project Readme for a detailed roadmap, but for now, I use it only to install and manage Ruby versions.&lt;/p&gt;

&lt;p&gt;The biggest point of difference is that &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt; doesn’t build Ruby from source on your machine. The project maintainers &lt;a href=&quot;https://github.com/spinel-coop/rv-ruby/releases&quot;&gt;host precompiled binaries&lt;/a&gt; for all popular operating systems and CPU architectures. This means that Ruby can be installed in under 2 seconds, rather than about 15 minutes.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;The binaries are built with YJIT and ZJIT, but not with jemalloc. You’ll need to inject that yourself with the &lt;code class=&quot;highlighter-rouge&quot;&gt;LD_PRELOAD&lt;/code&gt; environment variable.&lt;/p&gt;&lt;/aside&gt;

&lt;h2 id=&quot;installing-ruby-with-rv&quot;&gt;Installing Ruby with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Install the latest Ruby version with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt; using:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;rv ruby &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;or specify a version:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;rv ruby &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;4.0.6
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will download the correct precompiled binary for your system. This makes Ruby upgrades on servers an order of magnitude easier as you don’t have wait 15 minutes for the build, or worry about the additional CPU usage if it’s a production web server.&lt;/p&gt;

&lt;h2 id=&quot;switching-ruby-versions-with-rv&quot;&gt;Switching Ruby versions with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;When it comes to switching between different Ruby versions, &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt; takes the best bits of both &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; and combines them.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;rv shell&lt;/code&gt; command prints out instructions to integrate &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt; with your chosen shell. Once integrated, it automatically detects the active Ruby version using the &lt;code class=&quot;highlighter-rouge&quot;&gt;.ruby-version&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;.tools-versions&lt;/code&gt; file in your project, and selects it using environment variables.&lt;/p&gt;

&lt;p&gt;Additionally, you can use &lt;code class=&quot;highlighter-rouge&quot;&gt;rv run&lt;/code&gt; to run commands within a Ruby environment. This can be used to run a command in a different Ruby that what’s currently active, or to setup a Ruby environment where the shell integration doesn’t apply (such as, when a login shell isn’t available).&lt;/p&gt;

&lt;p&gt;I think this is an elegant solution that allows users to execute Ruby commands seamlessly in an interactive shell, while also providing a way to configure a Ruby environment explicitly, without &lt;em&gt;shimming&lt;/em&gt; the &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby&lt;/code&gt; command itself.&lt;/p&gt;

&lt;p&gt;See the &lt;a href=&quot;https://github.com/spinel-coop/rv&quot;&gt;project readme&lt;/a&gt; for detailed usage docs.&lt;/p&gt;

&lt;h2 id=&quot;rv-in-my-workflow&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt; in my workflow&lt;/h2&gt;

&lt;p&gt;I’ve switched all Ruby management to &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt; in my deployment tool: &lt;a href=&quot;https://codeberg.org/ayushn21/mimas&quot;&gt;Mimas&lt;/a&gt;. It simplified the server provisioning process, and reduced the time required to install a new Ruby to mere seconds.&lt;/p&gt;

&lt;p&gt;I’ve also started using &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt; for my local development environment. It took me just a few minutes to uninstall &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt; and get everything working with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;. I really can’t recommend it highly enough. Go and give it a try!&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/postgresql-server-setup-for-rails-apps&quot;&gt;PostgreSQL server setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/redis-server-setup-for-rails-apps&quot;&gt;Redis setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/deploying-rails-apps-to-a-server-using-mimas&quot;&gt;Deploying Rails apps using Mimas&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/generating-self-signed-certificates-for-web-services&quot;&gt;Generating self-signed certificates for web services&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">Zero-downtime Ruby deployment without Docker</title><link href="https://binarysolo.blog/zero-downtime-ruby-deployment-without-docker/" rel="alternate" type="text/html" title="Zero-downtime Ruby deployment without Docker" /><published>2026-07-31T00:00:00+01:00</published><updated>2026-07-31T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-07-31-deploying-ruby-apps.md</id><content type="html" xml:base="https://binarysolo.blog/zero-downtime-ruby-deployment-without-docker/">&lt;aside class=&quot;series_index&quot;&gt;
  &lt;h2&gt;Deploying Ruby apps to a server&lt;/h2&gt;
  &lt;ol&gt;
    &lt;li&gt;
      1. &amp;nbsp;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;2a. &lt;a href=&quot;/serving-ruby-apps-with-caddy-as-a-reverse-proxy&quot;&gt;Serving Ruby apps with Caddy as a reverse proxy&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;2b. &lt;a href=&quot;/falcon-as-a-web-server-for-ruby-applications&quot;&gt;Falcon as a web server for Ruby applications&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;3. &amp;nbsp;Zero-downtime Ruby deployment without Docker&lt;/li&gt;
  &lt;/ol&gt;
&lt;/aside&gt;

&lt;p&gt;In the final part of this series, we’ll build upon the setup created in &lt;a href=&quot;/serving-ruby-apps-with-caddy-as-a-reverse-proxy&quot;&gt;Part 2a&lt;/a&gt; and close the loop by deploying code from our machine to the server.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/serving-ruby-apps-with-caddy-as-a-reverse-proxy&quot;&gt;Part 2a&lt;/a&gt; covered the setup to serve Ruby apps, but that app was created on the server itself. To deploy code, we need a way to copy our project files to the server and reload the server process for the changes to take effect. Ideally, we also want to ensure the new code starts up correctly and is healthy, before switching traffic over to it (sometimes known as blue-green deploys).&lt;/p&gt;

&lt;p&gt;At the end of the post, I’ll introduce &lt;a href=&quot;https://codeberg.org/ayushn21/mimas&quot;&gt;Mimas&lt;/a&gt;, which automates everything discussed in this post. Hence, this post is largely theory and doesn’t contain code examples for every step. Although, this knowledge will stand you in good stead if you choose to build your own deployment tooling.&lt;/p&gt;

&lt;p&gt;Here are the steps required for zero-downtime blue-green deployments:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Copy the code to the server.&lt;/li&gt;
  &lt;li&gt;Start a new server process with the deployed code.&lt;/li&gt;
  &lt;li&gt;Health-check the new process.&lt;/li&gt;
  &lt;li&gt;Switch the reverse proxy to the new server’s Unix socket.&lt;/li&gt;
  &lt;li&gt;Gracefully stop the old process without dropping requests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s take this step-by-step.&lt;/p&gt;

&lt;h2 id=&quot;copying-code-to-the-server&quot;&gt;Copying code to the server&lt;/h2&gt;

&lt;p&gt;Heroku popularised the &lt;code class=&quot;highlighter-rouge&quot;&gt;git push&lt;/code&gt; technique to copy code to a server. Personally I’m not a fan of this method when deploying code to a VPS, because it uses the server as a build server. &lt;code class=&quot;highlighter-rouge&quot;&gt;git push&lt;/code&gt; will send your project as-is to the server, and any artifacts that require building, such as JavaScript assets, or an entire static site, will need to be done on the server.&lt;/p&gt;

&lt;p&gt;This creates more moving parts as build dependencies such as Node need to be installed on the server. I prefer to build the site locally, or on a CI server, and push the built files to the hosting server. The most efficient way to do this is to create a GZIP archive of all required files, and then use SCP to copy that to the server.&lt;/p&gt;

&lt;h2 id=&quot;starting-and-health-checking-new-server-process&quot;&gt;Starting and health-checking new server process&lt;/h2&gt;

&lt;p&gt;After we have the GZIPped code on the server, we can extract it using the &lt;code class=&quot;highlighter-rouge&quot;&gt;tar -xf&lt;/code&gt; command. To start the server process, we’ll create a new systemd service for the release. The way we set up the server in &lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Part 1&lt;/a&gt; means that this can be done without &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt; privileges.&lt;/p&gt;

&lt;p&gt;When the new systemd service has been created and started, we’ll make a &lt;code class=&quot;highlighter-rouge&quot;&gt;curl&lt;/code&gt; request to the new Unix socket to ensure it is running.&lt;/p&gt;

&lt;h2 id=&quot;switching-the-reverse-proxy-and-stopping-the-old-service&quot;&gt;Switching the reverse proxy and stopping the old service&lt;/h2&gt;

&lt;p&gt;When the new service has been health-checked, we can change the Caddyfile to point to the new Unix socket, and reload Caddy. Caddy does this seamlessly without downtime, and gracefully completes any requests in-flight.&lt;/p&gt;

&lt;p&gt;And finally, we need to stop and remove the old service. Both Puma and Falcon stop gracefully without dropping requests when &lt;code class=&quot;highlighter-rouge&quot;&gt;SIGTERM&lt;/code&gt; is sent to the process, so we won’t lose any requests here either.&lt;/p&gt;

&lt;p&gt;And that’s it, our new code will be up and running!&lt;/p&gt;

&lt;h2 id=&quot;automation&quot;&gt;Automation&lt;/h2&gt;

&lt;p&gt;Doing all the above steps manually is a recipe for disaster. With so many things going on, this needs to be automated for reliability. I’ve created a CLI tool called &lt;a href=&quot;https://codeberg.org/ayushn21/mimas&quot;&gt;Mimas&lt;/a&gt; which automates everything I’ve covered in this blog series, and is optimised for single server Ruby deployments.&lt;/p&gt;

&lt;h2 id=&quot;introducing-mimas&quot;&gt;Introducing Mimas&lt;/h2&gt;

&lt;p&gt;My goal with &lt;a href=&quot;https://codeberg.org/ayushn21/mimas&quot;&gt;Mimas&lt;/a&gt; was to create a single solution for end-to-end deployments, from securing the server, to configuring log rotation, and everything in between. These were my goals when building Mimas:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Securing the server, including creating the required user acccounts.&lt;/li&gt;
  &lt;li&gt;Installing and configuring Caddy.&lt;/li&gt;
  &lt;li&gt;Installing prerequisite software including Ruby.&lt;/li&gt;
  &lt;li&gt;Deploying multiple sites on a single server.&lt;/li&gt;
  &lt;li&gt;Deployment of static sites as well as Ruby apps.&lt;/li&gt;
  &lt;li&gt;Creating systemd services to automatically start and restart web services.&lt;/li&gt;
  &lt;li&gt;Zero-downtime and blue-green deploys.&lt;/li&gt;
  &lt;li&gt;Configure log rotation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s still early days so there are likely to be issues. All my static sites (including the blog you’re reading) are now hosted on a VPS and deployed using Mimas. I’ve also created an &lt;a href=&quot;https://codeberg.org/ayushn21/mimas-bridgetown&quot;&gt;official Bridgetown plugin&lt;/a&gt; for it, so you can deploy your Bridgetown static or hybrid apps to a VPS easily!&lt;/p&gt;

&lt;p&gt;While it works well with static sites and simple Rack apps, Rails support is currently untested. I’m planning to migrate one of my Rails apps off Render, and onto a VPS, so I will release an official Rails plugin for Mimas in the coming weeks based off that work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EDIT&lt;/strong&gt;: Mimas now has &lt;a href=&quot;http://codeberg.org/ayushn21/mimas-rails&quot;&gt;a Rails plugin&lt;/a&gt;. Here’s the &lt;a href=&quot;/deploying-rails-apps-to-a-server-using-mimas&quot;&gt;blog post describing it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Mimas is written in Ruby and works by running Bash commands and scripts over SSH. There’s nothing fancy going on under the hood. The code should be fairly easy to follow, and the &lt;a href=&quot;https://codeberg.org/ayushn21/mimas#readme&quot;&gt;Readme&lt;/a&gt; describes how it all works.&lt;/p&gt;

&lt;p&gt;Docker is far too heavy-handed for most projects, and PaaS providers can be quite expensive for small-scale and side projects. I hope that Mimas helps self-hosting become a bit more accessible as a free all-in-one solution.&lt;/p&gt;

&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/ruby-installation-and-management-with-rv&quot;&gt;Ruby version management with &lt;code class=&quot;highlighter-rouge&quot;&gt;rv&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/postgresql-server-setup-for-rails-apps&quot;&gt;PostgreSQL server setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/redis-server-setup-for-rails-apps&quot;&gt;Redis setup for Rails apps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/deploying-rails-apps-to-a-server-using-mimas&quot;&gt;Deploying Rails apps using Mimas&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/generating-self-signed-certificates-for-web-services&quot;&gt;Generating self-signed certificates for web services&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">Falcon as a web server for Ruby applications</title><link href="https://binarysolo.blog/falcon-as-a-web-server-for-ruby-applications/" rel="alternate" type="text/html" title="Falcon as a web server for Ruby applications" /><published>2026-07-24T00:00:00+01:00</published><updated>2026-07-24T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-07-24-2-falcon-as-a-web-server-for-ruby-applications.md</id><content type="html" xml:base="https://binarysolo.blog/falcon-as-a-web-server-for-ruby-applications/">&lt;aside class=&quot;series_index&quot;&gt;
  &lt;h2&gt;Deploying Ruby apps to a server&lt;/h2&gt;
  &lt;ol&gt;
    &lt;li&gt;
      1. &amp;nbsp;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;2a. &lt;a href=&quot;/serving-ruby-apps-with-caddy-as-a-reverse-proxy&quot;&gt;Serving Ruby apps with Caddy as a reverse proxy&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;2b. Falcon as a web server for Ruby applications&lt;/li&gt;
    &lt;li&gt;3. &amp;nbsp;&lt;a href=&quot;/zero-downtime-ruby-deployment-without-docker&quot;&gt;Zero-downtime Ruby deployment without Docker&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/aside&gt;

&lt;p&gt;In the &lt;a href=&quot;/serving-ruby-apps-with-caddy-as-a-reverse-proxy&quot;&gt;previous post&lt;/a&gt;, we covered how to install and run Caddy, and then use it as a reverse proxy to serve a Rack app, and discussed why this is the conventional configuration.&lt;/p&gt;

&lt;p&gt;However, &lt;a href=&quot;http://github.com/socketry/falcon&quot;&gt;Falcon&lt;/a&gt; aims to change this. It has a mode called Falcon Virtual designed specifically to face the internet and perform SSL termination and SNI resolution to serve multiple sites. It also supports HTTP/2 natively. It’s designed for high I/O throughput using Ruby Fibers making it efficient at serving static assets.&lt;/p&gt;

&lt;p&gt;That got me thinking that it would be great to serve Ruby apps using Ruby end-to-end. This post describes a strategy to serve a Rack app using Falcon, without an additional reverse proxy. And also, why that’s a really bad idea and I wouldn’t recommend it.&lt;/p&gt;

&lt;p&gt;To be clear, Falcon’s great, but the approach of serving a Ruby web app without a reverse proxy is wrong in my opinion, and I’ll explain why at the end.&lt;/p&gt;

&lt;p&gt;You’ll need a domain name that you own, and can point at your server to complete this guide.&lt;/p&gt;

&lt;h2 id=&quot;initial-setup&quot;&gt;Initial setup&lt;/h2&gt;

&lt;p&gt;If you worked through the previous post, and have Caddy installed on your server, stop the Caddy service as it will clash with Falcon. Falcon will need to bind to ports 80 and 443 to serve internet HTTP and HTTPS traffic.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Run as the `admin` user&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl stop caddy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Apart from that, this guide builds on the server setup described in &lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Part 1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ports lower than 1024 are privileged and can only be bound to by processes with elevated privileges. However, I’d prefer to run Falcon as the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user. That way, automated tools can manage it without needing to connect as &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;authbind&quot;&gt;Authbind&lt;/h3&gt;

&lt;p&gt;There’s a utility called &lt;code class=&quot;highlighter-rouge&quot;&gt;authbind&lt;/code&gt; which enables processes run by users without elevated privileges to bind to privileged ports.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;We didn’t use &lt;code class=&quot;highlighter-rouge&quot;&gt;authbind&lt;/code&gt; to run Caddy in the previous post because it isn’t compatible with it. It’s got something to do with the way the Go language handles networking. If you have a good explanation for why it is incompatible, &lt;a href=&quot;mailto:ayush@radioactivetoy.tech&quot;&gt;email me&lt;/a&gt;! I’d love to understand it better.&lt;/p&gt;&lt;/aside&gt;

&lt;p&gt;SSH to your server as &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt; and install &lt;code class=&quot;highlighter-rouge&quot;&gt;authbind&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh admin@&amp;lt;ip-address&amp;gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;authbind
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, we need to create files representing each port we’d like to allow a user to bind to, and give them the &lt;em&gt;execute&lt;/em&gt; permission on that file:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo touch&lt;/span&gt; /etc/authbind/byport/80
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo touch&lt;/span&gt; /etc/authbind/byport/443
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;setfacl &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; u:deploy:rx /etc/authbind/byport/80
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;setfacl &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; u:deploy:rx /etc/authbind/byport/443
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We create two files for the two HTTP ports and give the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user the necessary permissions. Using &lt;code class=&quot;highlighter-rouge&quot;&gt;authbind&lt;/code&gt;, we can now run a process under the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user that can bind to these ports.&lt;/p&gt;

&lt;h3 id=&quot;certbot&quot;&gt;Certbot&lt;/h3&gt;

&lt;p&gt;While you’re connected as &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt;, install &lt;code class=&quot;highlighter-rouge&quot;&gt;certbot&lt;/code&gt; to issue SSL certificates, as Falcon doesn’t do this automatically.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;snap &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--classic&lt;/span&gt; certbot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Exit the SSH session and reconnect as &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh deploy@&amp;lt;ip-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;a-basic-rack-app&quot;&gt;A basic Rack app&lt;/h2&gt;

&lt;aside&gt;
  &lt;p&gt;If you followed the previous post, and already have a Rack app, you can re-use that. There are some minor differences in setup, so ensure you still follow along closely.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;Create a new Rack app within a folder named after your domain name:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/http/example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;aside&gt;
  &lt;p&gt;This is also a good time to ensure you&apos;ve created a DNS A record to point your domain to the server&apos;s IP address.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;Scaffold an empty Ruby project:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/http/example.com
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle init
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;config.ru
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle add rack falcon
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a basic Rack app as shown below:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config.ru&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;content-type&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;text/plain&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;It is now &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;utc&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You’ll also need to create a Falcon config file that is executable:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;falcon.rb
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x falcon.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# falcon.rb&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/env falcon-host&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;falcon/environment/rack&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;service&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;example.com&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Falcon&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Rack&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;bootstrapping-an-ssl-certificate&quot;&gt;Bootstrapping an SSL certificate&lt;/h2&gt;

&lt;p&gt;This is where things start to get tricky because Falcon doesn’t automatically issue SSL certificates. We’ll use a utility called &lt;code class=&quot;highlighter-rouge&quot;&gt;certbot&lt;/code&gt; to issue certificates. Certbot makes a request to &lt;a href=&quot;https://letsencrypt.org&quot;&gt;Let’s Encrypt&lt;/a&gt;, which issues something called an &lt;em&gt;ACME challenge&lt;/em&gt;. This is an HTTP request to your server for a file certbot places at a conventional location. It’s used to validate that you own the domain and server.&lt;/p&gt;

&lt;p&gt;This leads to a catch-22 problem where we need an SSL certificate (even an invalid one), to be able to start Falcon so it can serve the ACME challenge used to issue the SSL certificate.&lt;/p&gt;

&lt;p&gt;We’ll solve this problem by:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Creating a self-signed SSL certificate and point Falcon at that in the absence of a valid certificate.&lt;/li&gt;
  &lt;li&gt;Handle the ACME challenge HTTP request using the self-signed certificate.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;issuing-a-self-signed-certificate&quot;&gt;Issuing a self-signed certificate&lt;/h3&gt;

&lt;p&gt;First, update the Falcon config so it looks for a live SSL certificate, and falls back to a self-signed certificate if one doesn’t exist:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# falcon.rb&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/env falcon-host&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;falcon/environment/rack&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;falcon/environment/tls&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;service&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rack.forkhandles.cc&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Falcon&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Rack&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Falcon&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TLS&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Required due to https://github.com/socketry/falcon/pull/355&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ssl_private_key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;OpenSSL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;PKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ssl_private_key_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Define a location for the self-signed certs used to bootstrap a valid cert&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lets_encrypt_bootstrap&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/home/deploy/.config/letsencrypt/bootstrap&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Define a location for the live certificates&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lets_encrypt_root&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/home/deploy/.config/letsencrypt/live&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Fallback to the self-signed cert if a valid cert doesn&apos;t exist&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ssl_certificate_path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;live_cert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lets_encrypt_root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authority&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fullchain.pem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;bootstrap_cert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lets_encrypt_bootstrap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authority&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fullchain.pem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exist?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;live_cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;live_cert&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_cert&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;ssl_private_key_path&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;live_pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lets_encrypt_root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authority&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;privkey.pem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;bootstrap_pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lets_encrypt_bootstrap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authority&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;privkey.pem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exist?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;live_pk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;live_pk&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bootstrap_pk&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create the directory structure for the certificates, and issue the self-signed certificate.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/.config/letsencrypt/bootstrap
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/.config/letsencrypt/live
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/.config/letsencrypt/log
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/.config/letsencrypt/lib
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; ~/.config/letsencrypt/bootstrap/example.com
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/.config/letsencrypt/bootstrap/example.com
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;openssl req &lt;span class=&quot;nt&quot;&gt;-x509&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-keyout&lt;/span&gt; privkey.pem &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-out&lt;/span&gt; fullchain.pem &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-days&lt;/span&gt; 365 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-nodes&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;-subj&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/CN=example.com&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;aside&gt;
  &lt;p&gt;Ensure you’re replacing &lt;code class=&quot;highlighter-rouge&quot;&gt;example.com&lt;/code&gt; with your own domain name in all code examples.&lt;/p&gt;&lt;/aside&gt;

&lt;h3 id=&quot;responding-to-the-acme-challenge&quot;&gt;Responding to the ACME challenge&lt;/h3&gt;

&lt;p&gt;Next, we need to handle the ACME challenge. Implement a Rack middleware to match and respond to the challenge’s HTTP request:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config.ru&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AcmeChallenge&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;MATCHER&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/\A\/\.well-known\/acme-challenge\/([\w-]+)/&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@app&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PATH_INFO&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;AcmeChallenge&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MATCHER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;challenge_file&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__dir__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;.well-known&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;acme-challenge&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exist?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;challenge_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;challenge_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]]&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;404&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;vi&quot;&gt;@app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;content-type&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;text/plain&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;It is now &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;utc&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AcmeChallenge&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The middleware will detect a challenge request, and read the file placed by certbot in the conventional location and return it to fulfil the challenge.&lt;/p&gt;

&lt;p&gt;We can now finally run Falcon:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;authbind &lt;span class=&quot;nt&quot;&gt;--deep&lt;/span&gt; falcon virtual /home/deploy/http/&lt;span class=&quot;k&quot;&gt;**&lt;/span&gt;/falcon.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The glob pattern should find the &lt;code class=&quot;highlighter-rouge&quot;&gt;falcon.rb&lt;/code&gt; files for all sites on the server and start them. It will use the domain specified as the service name in &lt;code class=&quot;highlighter-rouge&quot;&gt;falcon.rb&lt;/code&gt; to match requests to the correct site.&lt;/p&gt;

&lt;p&gt;With the server running, create a second SSH connection as &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; and run certbot:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;certbot certonly &lt;span class=&quot;nt&quot;&gt;--webroot&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--agree-tos&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--email&lt;/span&gt; &amp;lt;your email&amp;gt; &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; ~/http/example.com &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; example.com  &lt;span class=&quot;nt&quot;&gt;--work-dir&lt;/span&gt; ~/.config/letsencrypt/lib &lt;span class=&quot;nt&quot;&gt;--logs-dir&lt;/span&gt; ~/.config/letsencrypt/log &lt;span class=&quot;nt&quot;&gt;--config-dir&lt;/span&gt; ~/.config/letsencrypt/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Certbot should issue your certificate and Falcon should automatically detect it after a few seconds. Once the certificate is issued, certbot will automatically renew it.&lt;/p&gt;

&lt;p&gt;Your Rack app should now be accessible from the internet, using your domain name, with a valid SSL certificate.&lt;/p&gt;

&lt;h2 id=&quot;a-systemd-service&quot;&gt;A systemd service&lt;/h2&gt;

&lt;p&gt;You can create a &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt; service for Falcon Virtual as shown below:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Unit]
Description=Falcon Virtual
After=network.target

[Service]
Type=notify
NotifyAccess=all
User=deploy
ExecStart=/usr/bin/bash -lc &apos;authbind --deep falcon virtual %h/http/**/falcon.rb&apos;
Restart=always

[Install]
WantedBy=default.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Place the above contents in &lt;code class=&quot;highlighter-rouge&quot;&gt;~/.config/systemd/user/falcon-virtual.service&lt;/code&gt;, and then start the service using:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;systemctl &lt;span class=&quot;nt&quot;&gt;--user&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--now&lt;/span&gt; falcon-virtual
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;why-this-setup-is-a-bad-idea&quot;&gt;Why this setup is a bad idea&lt;/h2&gt;

&lt;p&gt;If you followed along the previous post which used Caddy, as well as this one, you probably already understand why I don’t recommend this approach.&lt;/p&gt;

&lt;p&gt;Beyond the added fiddliness to manage SSL certificates and the ACME challenge, it also makes zero-downtime deploys impossible when upgrading Ruby. Falcon Virtual starts a main process, and then forks off processes for each of your sites. While we can reload code without stopping processes, it’s impossible to change the Ruby binary without stopping the main Falcon Virtual process.&lt;/p&gt;

&lt;p&gt;Also, Falcon is an application server. It’s pretty damn great at being an application server. Caddy is a purpose-built web server and reverse proxy. Falcon will always be more tricky to use as a web server than Caddy.&lt;/p&gt;

&lt;p&gt;I recommend keeping your reverse proxy independent from your Ruby processes as this is the most flexible setup.&lt;/p&gt;

&lt;p&gt;In the next, and &lt;a href=&quot;/zero-downtime-ruby-deployment-without-docker&quot;&gt;final part&lt;/a&gt; of this series, we’ll look at deploying Ruby apps to a server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3.&lt;/strong&gt; &lt;a href=&quot;/zero-downtime-ruby-deployment-without-docker&quot;&gt;Zero-downtime Ruby deployment without Docker&lt;/a&gt;&lt;/p&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">Serving Ruby apps with Caddy as a reverse proxy</title><link href="https://binarysolo.blog/serving-ruby-apps-with-caddy-as-a-reverse-proxy/" rel="alternate" type="text/html" title="Serving Ruby apps with Caddy as a reverse proxy" /><published>2026-07-24T00:00:00+01:00</published><updated>2026-07-24T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-07-24-1-serving-ruby-apps-using-caddy.md</id><content type="html" xml:base="https://binarysolo.blog/serving-ruby-apps-with-caddy-as-a-reverse-proxy/">&lt;aside class=&quot;series_index&quot;&gt;
  &lt;h2&gt;Deploying Ruby apps to a server&lt;/h2&gt;
  &lt;ol&gt;
    &lt;li&gt;
      1. &amp;nbsp;&lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Server setup for Ruby web applications&lt;/a&gt;
    &lt;/li&gt;
    &lt;li&gt;2a. Serving Ruby apps with Caddy as a reverse proxy&lt;/li&gt;
    &lt;li&gt;2b. &lt;a href=&quot;/falcon-as-a-web-server-for-ruby-applications&quot;&gt;Falcon as a web server for Ruby applications&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;3. &amp;nbsp;&lt;a href=&quot;/zero-downtime-ruby-deployment-without-docker&quot;&gt;Zero-downtime Ruby deployment without Docker&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/aside&gt;

&lt;p&gt;In this post, we’ll cover how to serve a &lt;a href=&quot;https://github.com/rack/rack&quot;&gt;Rack&lt;/a&gt; app using &lt;a href=&quot;http://caddyserver.com&quot;&gt;Caddy&lt;/a&gt; as a reverse proxy. We won’t be covering how to deploy code to the server as yet. We’ll create the Rack app on the server to handle basic HTTP requests.&lt;/p&gt;

&lt;p&gt;This guide builds upon the server setup in &lt;a href=&quot;/server-setup-for-ruby-web-applications&quot;&gt;Part 1 of this series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You’ll need a domain name that you own, and can point at your server to follow along with this guide.&lt;/p&gt;

&lt;h2 id=&quot;why-use-a-reverse-proxy&quot;&gt;Why use a reverse proxy?&lt;/h2&gt;

&lt;p&gt;Ruby apps are conventionally served through a reverse proxy. Popular Ruby app servers such as Puma aren’t focused on being &lt;em&gt;web servers&lt;/em&gt;. Their focus is on running Ruby code, and not on web server features like load balancing, SSL termination, response compression, or serving static assets. While Ruby servers may have those features, it’s not their primary focus, and hence they’re fiddly to use.&lt;/p&gt;

&lt;p&gt;A reverse proxy handles all the &lt;em&gt;web server&lt;/em&gt; related tasks like SSL termination and serving static files, while handing off HTTP requests to the application server. Most popular Ruby app servers also don’t support HTTP/2, which provides a significant performance gain when serving static files.&lt;/p&gt;

&lt;h2 id=&quot;installing-caddy&quot;&gt;Installing Caddy&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://caddyserver.com&quot;&gt;Caddy&lt;/a&gt; is a fantastic new-ish web server that can also be used as a reverse proxy. The documentation is accessible and thorough, and it can be configured with ease unlike older alternatives like Nginx.&lt;/p&gt;

&lt;p&gt;SSH to your server as the &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt; user:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh admin@&amp;lt;ip-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Caddy can be installed using the system package manager (in this case &lt;code class=&quot;highlighter-rouge&quot;&gt;apt&lt;/code&gt;, since we’re on Ubuntu). However, that also installs a systemd service, so I prefer downloading the binary to customise the service to my preference.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Update the apt source for Caddy&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; debian-keyring debian-archive-keyring apt-transport-https
curl &lt;span class=&quot;nt&quot;&gt;-1sLf&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;gpg &lt;span class=&quot;nt&quot;&gt;--dearmor&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-tty&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--batch&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--yes&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl &lt;span class=&quot;nt&quot;&gt;-1sLf&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/caddy-stable.list
&lt;span class=&quot;nb&quot;&gt;sudo chmod &lt;/span&gt;o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
&lt;span class=&quot;nb&quot;&gt;sudo chmod &lt;/span&gt;o+r /etc/apt/sources.list.d/caddy-stable.list
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get update

&lt;span class=&quot;c&quot;&gt;# Download and install the Caddy binary&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; ~/tmp
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/tmp
apt-get download caddy
&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; caddy-unpack
dpkg &lt;span class=&quot;nt&quot;&gt;-x&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;find caddy&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;.deb&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; ~/tmp/caddy-unpack
&lt;span class=&quot;nb&quot;&gt;sudo mv&lt;/span&gt; ~/tmp/caddy-unpack/usr/bin/caddy /usr/local/bin/caddy
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rf&lt;/span&gt; ~/tmp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Caddy needs to bind to the standard ports 80 and 443 to serve HTTP and HTTPS traffic. These are privileged ports, meaning only processes running as root (or with elevated privileges using &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt;) can bind to them. Alternatively, a process has to be given access to bind to those ports.&lt;/p&gt;

&lt;p&gt;As such, the Caddy &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt; service has to be created using the &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt; user with elevated privileges, even though it will run under the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user. The service will contain a definition giving the process the releavant permissions.&lt;/p&gt;

&lt;p&gt;But, before we can install the service, let’s create a Caddyfile containing a base configuration as the server can’t start without a config file. The Caddyfile needs to be owned by and under the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user’s home directory so it can be modified without connecting as &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of reconnecting as &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt;, create the config files under the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user using &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt;, and change their ownership:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /home/deploy/http
&lt;span class=&quot;nb&quot;&gt;sudo touch&lt;/span&gt; /home/deploy/http/Caddyfile
&lt;span class=&quot;nb&quot;&gt;sudo touch&lt;/span&gt; /home/deploy/http/caddy.global

&lt;span class=&quot;nb&quot;&gt;sudo cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &amp;gt;&amp;gt; /home/deploy/http/caddy.global
{
  servers {
    protocols h1 h2
  }
}
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &amp;gt;&amp;gt; /home/deploy/http/Caddyfile
import caddy.global
import */Caddyfile
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo chown &lt;/span&gt;deploy:deploy &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; /home/deploy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;caddy.global&lt;/code&gt; contains global settings that apply across all websites on the server. I restrict it to HTTP/1 and HTTP/2 because I find HTTP/3 causes weird connection issues in browsers in between deploys.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;Caddyfile&lt;/code&gt; is the main configuration file we’ll pass into Caddy. It loads the global settings and the individual Caddyfiles of all our websites.&lt;/p&gt;

&lt;p&gt;The Caddy configuration is held in memory, so if you deploy a new site and need Caddy to pick up it’s config, you’ll need to &lt;code class=&quot;highlighter-rouge&quot;&gt;reload&lt;/code&gt; the service. Caddy handles this gracefully with zero downtime.&lt;/p&gt;

&lt;p&gt;Then, give the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user just enough &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt; access to reload and monitor the caddy service.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo touch&lt;/span&gt; /etc/sudoers.d/deploy

&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &amp;gt;&amp;gt; /etc/sudoers.d/deploy
deploy &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /etc/hostname&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;=(root) NOPASSWD: /bin/systemctl reload caddy
deploy &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /etc/hostname&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;=(root) NOPASSWD: /bin/systemctl status caddy
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, create the Caddy service file:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &amp;gt; /etc/systemd/system/caddy.service
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=deploy
ExecStart=/usr/local/bin/caddy run --environ --config /home/deploy/http/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config /home/deploy/http/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;AmbientCapabilities&lt;/code&gt; gives it access to bind to ports 80 and 443 to serve HTTP and HTTPS requests. This access can only be given by a user with elevated privileges, which is why we can’t create this service within the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user.&lt;/p&gt;

&lt;p&gt;The Ruby app’s service will be created in the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user as it doesn’t need elevated privileges.&lt;/p&gt;

&lt;p&gt;Enable and start the service:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl daemon-reload &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl &lt;span class=&quot;nb&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--now&lt;/span&gt; caddy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Caddy should have started running. Verify it with:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl status caddy
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;-kv&lt;/span&gt; http://localhost:2019/config/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The Caddyfile is blank, so it won’t serve anything yet. Let’s create an app for it to serve.&lt;/p&gt;

&lt;h2 id=&quot;a-basic-rack-app&quot;&gt;A basic Rack app&lt;/h2&gt;

&lt;p&gt;Disconnect your SSH session and reconnect as the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh deploy@&amp;lt;ip-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a folder for your site named after your domain name:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/http/example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;aside&gt;
  &lt;p&gt;This is also a good time to ensure you&apos;ve created a DNS A record to point your domain at the server&apos;s IP address.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;Create a Caddyfile specific to this site, and scaffold an empty Ruby project:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/http/example.com
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;Caddyfile
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle init
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;config.ru
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle add rack falcon
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We’re using Falcon as our server here, but you can use Puma if you wish.&lt;/p&gt;

&lt;p&gt;Open up a text editor (I use &lt;code class=&quot;highlighter-rouge&quot;&gt;vim&lt;/code&gt;), and create a basic Rack app as shown below:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config.ru&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;content-type&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;text/plain&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;It is now &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;utc&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You’ll also need to create a Falcon config file:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;falcon.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# falcon.rb&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/env falcon-host&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;falcon/environment/rack&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;service&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;example.com&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Falcon&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Rack&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;rackup_path&lt;/span&gt;   &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;expand_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;./config.ru&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ipc_path&lt;/span&gt;      &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;expand_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;./application.sock&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;scheme&lt;/span&gt;        &lt;span class=&quot;s2&quot;&gt;&quot;http&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;protocol&lt;/span&gt;      &lt;span class=&quot;no&quot;&gt;Async&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTTP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Protocol&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTTP&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The Falcon configuration sets binds the app to a Unix socket at &lt;code class=&quot;highlighter-rouge&quot;&gt;application.sock&lt;/code&gt;. It also sets the scheme as &lt;code class=&quot;highlighter-rouge&quot;&gt;http&lt;/code&gt; since Caddy handles SSL termination, and the &lt;code class=&quot;highlighter-rouge&quot;&gt;protocol&lt;/code&gt; declaration automatically chooses between HTTP/1.1 and HTTP/2 depending on the request.&lt;/p&gt;

&lt;p&gt;We run the app on a Unix socket to avoid port clashes with other services. When there are multiple web apps, or multiple instances of an app running on a single server, managing port numbers between all of them would be impossible.&lt;/p&gt;

&lt;p&gt;At this point, you can do a quick test to check the app can be run:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;falcon host falcon.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a second SSH session, change to the site’s directory, and use curl to check if the server is running:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;--unix-socket&lt;/span&gt; application.sock http://localhost/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If the request succeeds, close the second SSH session, go back to the first one, and use &lt;code class=&quot;highlighter-rouge&quot;&gt;Ctrl+C&lt;/code&gt; to stop the server. Now that we know it works, we can create a &lt;code class=&quot;highlighter-rouge&quot;&gt;systemd&lt;/code&gt; service to start it automatically and keep it running.&lt;/p&gt;

&lt;h2 id=&quot;running-the-app-using-a-systemd-service&quot;&gt;Running the app using a systemd service&lt;/h2&gt;

&lt;p&gt;Create the service file:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/.config/systemd/user
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch&lt;/span&gt; ~/.config/systemd/user/example.com.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Add the below code block to the service file:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Unit]
Description=example.com Web Service
After=network.target

[Service]
Type=notify
NotifyAccess=all
User=deploy
WorkingDirectory=/home/deploy/http/example.com
ExecStart=/usr/bin/env bash -lc &apos;bundle exec falcon host falcon.rb&apos;
KillMode=mixed
TimeoutStopSec=60
Restart=always

[Install]
WantedBy=default.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A couple of things to note in the above service:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;bash -lc&lt;/code&gt; is used to start the server. This loads &lt;code class=&quot;highlighter-rouge&quot;&gt;~/.profile&lt;/code&gt; ensuring &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; loads and the environment variables pointing to the Ruby binary and Gems are set.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;KillMode=mixed&lt;/code&gt; sends &lt;code class=&quot;highlighter-rouge&quot;&gt;SIGTERM&lt;/code&gt; only to the main process when the service is stopped. This allows the server to stop gracefully without dropping requests. If it fails to exit within the timeout, then &lt;code class=&quot;highlighter-rouge&quot;&gt;SIGKILL&lt;/code&gt; will be sent to the main process as well as child processes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start the service:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;systemctl &lt;span class=&quot;nt&quot;&gt;--user&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--now&lt;/span&gt; example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Check that it is running using &lt;code class=&quot;highlighter-rouge&quot;&gt;curl&lt;/code&gt; as we did earlier. We can’t access this via the internet as yet because we haven’t pointed Caddy to it.&lt;/p&gt;

&lt;h2 id=&quot;configuring-the-caddy-reverse-proxy&quot;&gt;Configuring the Caddy reverse proxy&lt;/h2&gt;

&lt;p&gt;Configure Caddy to act as a reverse proxy to the Unix socket for your domain name:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# example.com/Caddyfile

example.com {
  reverse_proxy unix///home/deploy/http/example.com/application.sock
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Reload Caddy:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl reload caddy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can do this as the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user despite it being a &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt; command because we gave it specific access for it earlier in this guide.&lt;/p&gt;

&lt;p&gt;Your Rack app should now be available at your domain!&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Rack underpins every popular Ruby web framework and server, and hence you should be able to use this same techniqe to serve apps built with Rails, Hanami, Roda, or any other framework.&lt;/p&gt;

&lt;p&gt;In the next part, we’ll look at an alternate approach where Falcon itself is internet facing, and used for SSL termination and as a reverse proxy for multiple websites. We’ll also discuss why I don’t recommend that approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2b.&lt;/strong&gt; &lt;a href=&quot;/falcon-as-a-web-server-for-ruby-applications&quot;&gt;Falcon as a web server for Ruby applications&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3.&lt;/strong&gt; &lt;a href=&quot;/zero-downtime-ruby-deployment-without-docker&quot;&gt;Zero-downtime Ruby deployment without Docker&lt;/a&gt;&lt;/p&gt;</content><author><name>Ayush Newatia</name></author></entry><entry><title type="html">Server setup for Ruby web applications</title><link href="https://binarysolo.blog/server-setup-for-ruby-web-applications/" rel="alternate" type="text/html" title="Server setup for Ruby web applications" /><published>2026-07-23T00:00:00+01:00</published><updated>2026-07-23T00:00:00+01:00</updated><id>repo://posts.collection/_posts/2026-07-23-vps-setup-for-ruby-web-applications.md</id><content type="html" xml:base="https://binarysolo.blog/server-setup-for-ruby-web-applications/">&lt;aside class=&quot;series_index&quot;&gt;
  &lt;h2&gt;Deploying Ruby apps to a server&lt;/h2&gt;
  &lt;ol&gt;
    &lt;li&gt;
      1. &amp;nbsp;Server setup for Ruby web applications
    &lt;/li&gt;
    &lt;li&gt;2a. &lt;a href=&quot;/serving-ruby-apps-with-caddy-as-a-reverse-proxy&quot;&gt;Serving Ruby apps with Caddy as a reverse proxy&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;2b. &lt;a href=&quot;/falcon-as-a-web-server-for-ruby-applications&quot;&gt;Falcon as a web server for Ruby applications&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;3. &amp;nbsp;&lt;a href=&quot;/zero-downtime-ruby-deployment-without-docker&quot;&gt;Zero-downtime Ruby deployment without Docker&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/aside&gt;

&lt;p&gt;The ease of using Heroku and other similar platforms means a generation of developers, including me, have never really needed to setup and manage servers during our career. As such, I was a bit afraid of server admin and self-hosting my apps.&lt;/p&gt;

&lt;p&gt;Sadly, with the enshittifcation of PaaS providers, and an upward trend in pricing, I could no longer ignore the benefits of self-hosting. For example, &lt;a href=&quot;https://scattergun.email&quot;&gt;Scattergun&lt;/a&gt; is one of my side projects with exactly $0 ARR. I’m paying $49/month to host it on Render.com. It needs 1 web service, 1 job runner, a Postgres database, and a Redis instance. All very basic for a Rails app, and at that level, I can probably run it on a single €5.49/month Hetzner box.&lt;/p&gt;

&lt;p&gt;The trade-off for the lower price is that I need to do more ops work myself, but again, for small to medium sized apps, I feel this is a worthy trade-off with an efficient deployment setup.&lt;/p&gt;

&lt;p&gt;Tools like Kamal have emerged recently to simplify self-hosting. However, nothing tackles the problem end-to-end, from securing the server, to log rotation, to database backups etc.&lt;/p&gt;

&lt;p&gt;Kamal also uses Docker, which I’m not keen on as it’s a bit of a behemoth. It makes a lot of sense when you’re deploying to a fleet of servers, but I’ll be deploying to a single server and I feel it’s overkill for that.&lt;/p&gt;

&lt;p&gt;This is the first of a series of posts where I’ll desribe my self-hosting setup for Ruby (and Rails) applications that covers the process of deploying apps to a single server end-to-end.&lt;/p&gt;

&lt;p&gt;The first step towards self-hosting is to secure the server. In this post I’ll describe the security steps I like to take when I create a new VPS, and the software I install to run Ruby apps.&lt;/p&gt;

&lt;h2 id=&quot;creating-a-vps&quot;&gt;Creating a VPS&lt;/h2&gt;

&lt;p&gt;Create a VPS using your preferred cloud provider. I personally use &lt;a href=&quot;https://hetzner.com&quot;&gt;Hetzner&lt;/a&gt;. This guide is based on Ubuntu 26.04, but you should be able to adapt it for other distros.&lt;/p&gt;

&lt;p&gt;Ensure you configure an SSH key for &lt;code class=&quot;highlighter-rouge&quot;&gt;root&lt;/code&gt; access, rather than using a password. Here’s &lt;a href=&quot;/managing-ssh-key-pairs-for-server-access/&quot;&gt;my blog post on managing SSH keys for server access&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;upgrade-packages-and-enable-firewall&quot;&gt;Upgrade packages and enable firewall&lt;/h2&gt;

&lt;p&gt;SSH to your server as &lt;code class=&quot;highlighter-rouge&quot;&gt;root&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh root@&amp;lt;ip-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, update all system packages:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;apt-get update &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; upgrade
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, enable the firewall so only SSH, HTTP, and HTTPS ports are open:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ufw default deny incoming
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ufw default allow outgoing
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ufw allow OpenSSH
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ufw allow http
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ufw allow https
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ufw &lt;span class=&quot;nt&quot;&gt;--force&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;creating-users-and-restricting-permissions&quot;&gt;Creating users and restricting permissions&lt;/h2&gt;

&lt;p&gt;I create two users on my web servers: &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt;. &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt; will have &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt; access, and will only be used for the initial server setup. After a server is provisioned, it will never again be used by an automated tool. &lt;code class=&quot;highlighter-rouge&quot;&gt;root&lt;/code&gt; login is completely disabled.&lt;/p&gt;

&lt;p&gt;Automated tools will connect as &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; to deploy the apps and run related tasks. It will not have &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt; access as I’m not comfortable with automated tools running &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt; commands, or with web applications running under a user that can gain elevated privileges.&lt;/p&gt;

&lt;p&gt;Password login is insecure, and maintaing passwords for multiple servers is unwieldy, so we’ll restrict it and &lt;a href=&quot;/managing-ssh-key-pairs-for-server-access/&quot;&gt;use SSH keys instead&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create the admin user:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Add a new user, set the default shell,&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# and create a home directory&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;useradd admin &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; /bin/bash &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Disable password login&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;passwd &lt;span class=&quot;nt&quot;&gt;-ld&lt;/span&gt; admin

&lt;span class=&quot;c&quot;&gt;# Sync the private keys to allow SSH access&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;rsync &lt;span class=&quot;nt&quot;&gt;--archive&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--chown&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;admin:admin ~/.ssh /home/admin

&lt;span class=&quot;c&quot;&gt;# Allow passwordless sudo for `admin`&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%admin &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /etc/hostname&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=(root) NOPASSWD:ALL&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /etc/sudoers.d/admin

&lt;span class=&quot;c&quot;&gt;# Validate the modified sudoers file&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;visudo &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /etc/sudoers.d/admin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;aside&gt;
  &lt;p&gt;While passwordless sudo access might seem counter-intuitive, it allows
administrators and scripts to intentionally elevate privileges when required. We can’t require a password because we’ve disabled password logins in favour of SSH keys.&lt;/p&gt;

&lt;p&gt;This is another reason why automated deployment tools will only ever connect as &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt;.&lt;/p&gt;&lt;/aside&gt;

&lt;p&gt;Disable root login completely as the &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt; user can now use &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt; to gain root privileges. Password authentication is also disabled at system level in the same file.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch&lt;/span&gt; /etc/ssh/sshd_config.d/user.conf
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &amp;gt;&amp;gt; /etc/ssh/sshd_config.d/user.conf
PermitRootLogin no
PasswordAuthentication no
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING

&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;systemctl restart ssh
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;passwd &lt;span class=&quot;nt&quot;&gt;-ld&lt;/span&gt; root
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, create the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;useradd deploy &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; /bin/bash &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;passwd &lt;span class=&quot;nt&quot;&gt;-ld&lt;/span&gt; deploy

&lt;span class=&quot;c&quot;&gt;# Enable lingering so the user&apos;s systemd services keep running&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# when the user doesn&apos;t have an active session.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;loginctl enable-linger deploy

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;rsync &lt;span class=&quot;nt&quot;&gt;--archive&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--chown&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;deploy:deploy ~/.ssh /home/deploy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Since the &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; user won’t have &lt;code class=&quot;highlighter-rouge&quot;&gt;sudo&lt;/code&gt; access, it can’t create &lt;a href=&quot;https://en.wikipedia.org/wiki/Systemd&quot;&gt;systemd&lt;/a&gt; services in the system-wide location: &lt;code class=&quot;highlighter-rouge&quot;&gt;/etc/systemd/system&lt;/code&gt;. As such, we’ll create them in the user-specific location: &lt;code class=&quot;highlighter-rouge&quot;&gt;~/.config/systemd/user/&lt;/code&gt;. By default, these services start when the user starts a session and stop after the session concludes. Enabling &lt;em&gt;lingering&lt;/em&gt; as demonstrated above ensures services start on system boot and keep running even when the user isn’t connected.&lt;/p&gt;

&lt;h2 id=&quot;additional-security-steps&quot;&gt;Additional security steps&lt;/h2&gt;

&lt;p&gt;The below snippet updates several kernel and system settings to harden it against network and file-system attack vectors. I’ve adapted the settings from &lt;a href=&quot;https://gist.github.com/rameerez/238927b78f9108a71a77aed34208de11&quot;&gt;this script by Javier Ramirez&lt;/a&gt;. I won’t get into the details as there’s quite a lot, but you can search for each individual setting to learn more about it if you’re curious.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &amp;gt; /etc/sysctl.d/network-security.conf
# Disable ICMP redirects.
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0

# ICMP noise hygiene.
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1

# Drop spoofed/martian packets at the door.
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0

# SYN-flood resilience as the server handles
# port 80/443 traffic from the internet.
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5

# Hide kernel internals from unprivileged eyes (kernel pointer leaks, dmesg,
# perf side channels) and disable unprivileged eBPF.
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.perf_event_paranoid = 3
kernel.unprivileged_bpf_disabled = 1
net.core.bpf_jit_harden = 2

# Only processes with CAP_SYS_PTRACE may ptrace.
kernel.yama.ptrace_scope = 2

# Block runtime kernel replacement via kexec (one-way until reboot).
# CAVEAT: if you ever set up kdump crash dumps, remove this line first.
kernel.kexec_load_disabled = 1

# Filesystem link/dump hardening.
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.suid_dumpable = 0

# Turn off Martian logging
net.ipv4.conf.all.log_martians = 0
net.ipv4.conf.default.log_martians = 0
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING

&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;sysctl &lt;span class=&quot;nt&quot;&gt;--system&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For additional security, install &lt;code class=&quot;highlighter-rouge&quot;&gt;fail2ban&lt;/code&gt; to block IPs trying to brute force SSH access to the server:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;fail2ban &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch&lt;/span&gt; /etc/fail2ban/jail.local
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;STRING&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &amp;gt;&amp;gt; /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1d
findtime = 10m
maxretry = 5

[sshd]
enabled = true
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STRING

&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;systemctl &lt;span class=&quot;nb&quot;&gt;enable &lt;/span&gt;fail2ban
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And finally, install &lt;code class=&quot;highlighter-rouge&quot;&gt;molly-guard&lt;/code&gt; to prevent accidental reboots. This requires the user to enter the server’s hostname to reboot it, or run &lt;code class=&quot;highlighter-rouge&quot;&gt;reboot.no-molly-guard&lt;/code&gt; to bypass the check. It adds an additional layer of scrutiny to avoid accidental reboots.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;molly-guard
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Reboot the server for changes to take effect:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;reboot.no-molly-guard
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;root&lt;/code&gt; connections to the server will no longer be possible after reboot. Use &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt; instead. You may wish to add some additional layers of security, but this provides a good baseline.&lt;/p&gt;

&lt;p&gt;Here’s a script with all the above steps: &lt;a href=&quot;https://gist.github.com/ayushn21/41b1ceb4fc2651d7d849c432c0a4fa05&quot;&gt;https://gist.github.com/ayushn21/41b1ceb4fc2651d7d849c432c0a4fa05&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, we’ll install the prerequisite software to run Ruby apps.&lt;/p&gt;

&lt;h2 id=&quot;installing-ruby-prerequisites&quot;&gt;Installing Ruby prerequisites&lt;/h2&gt;

&lt;p&gt;Re-connect to your server as the &lt;code class=&quot;highlighter-rouge&quot;&gt;admin&lt;/code&gt; user:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh admin@&amp;lt;ip-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Install Ruby’s prerequisites:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;curl jq xz-utils build-essential zlib1g-dev &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  libyaml-dev libssl-dev libncurses-dev libffi-dev &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  rustc libjemalloc-dev &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The above list includes &lt;code class=&quot;highlighter-rouge&quot;&gt;rustc&lt;/code&gt; to enable &lt;a href=&quot;https://docs.ruby-lang.org/en/master/jit/yjit_md.html&quot;&gt;YJIT&lt;/a&gt;, and &lt;code class=&quot;highlighter-rouge&quot;&gt;libjemalloc-dev&lt;/code&gt; to use &lt;code class=&quot;highlighter-rouge&quot;&gt;jemalloc&lt;/code&gt; for enhanced memory management.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;I got the list of prerequisites from the &lt;a href=&quot;https://github.com/postmodern/ruby-install/blob/master/share/ruby-install/ruby/dependencies.sh&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt;&lt;/a&gt; repo.&lt;/p&gt;&lt;/aside&gt;

&lt;p&gt;I prefer using &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby-install&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; to install and manage multiple Ruby versions, rather than alternatives such as &lt;code class=&quot;highlighter-rouge&quot;&gt;rbenv&lt;/code&gt;. I feel it has a cleaner approach as it works by configuring environment variables, rather than using shims to intercept commands and direct them to the correct Ruby binary.&lt;/p&gt;

&lt;p&gt;Install both utilities:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install ruby-install&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;wget https://github.com/postmodern/ruby-install/releases/download/v0.10.2/ruby-install-0.10.2.tar.gz
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tar&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-xzvf&lt;/span&gt; ruby-install-0.10.2.tar.gz
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ruby-install-0.10.2/
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;make &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ..
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rm &lt;/span&gt;ruby-install-0.10.2.tar.gz
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rf&lt;/span&gt; ruby-install-0.10.2/

&lt;span class=&quot;c&quot;&gt;# Install chruby&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;wget https://github.com/postmodern/chruby/releases/download/v0.3.9/chruby-0.3.9.tar.gz
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tar&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-xzvf&lt;/span&gt; chruby-0.3.9.tar.gz
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;chruby-0.3.9/
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;make &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ..
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rm &lt;/span&gt;chruby-0.3.9.tar.gz
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rf&lt;/span&gt; chruby-0.3.9/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Disconnect from the &lt;code class=&quot;highlighter-rouge&quot;&gt;admin user&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;installing-ruby&quot;&gt;Installing Ruby&lt;/h2&gt;

&lt;p&gt;Install Ruby as &lt;code class=&quot;highlighter-rouge&quot;&gt;deploy&lt;/code&gt; as that’s the user the apps will be run under.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh deploy@&amp;lt;ip-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ruby-install &lt;span class=&quot;nt&quot;&gt;--no-install-deps&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--cleanup&lt;/span&gt; ruby &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--enable-yjit&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--with-jemalloc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--disable-install-rdoc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Building Ruby will take a bit of time. After it completes, enable &lt;code class=&quot;highlighter-rouge&quot;&gt;chruby&lt;/code&gt; by sourcing its scripts in your &lt;code class=&quot;highlighter-rouge&quot;&gt;.profile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &amp;gt;&amp;gt; ~/.profile

# Use chruby to auto-switch Ruby versions
source /usr/local/share/chruby/chruby.sh
source /usr/local/share/chruby/auto.sh
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Finally, source the scripts into your current session, set a default Ruby, and setup bundler.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; /usr/local/share/chruby/chruby.sh
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; /usr/local/share/chruby/auto.sh

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ~/.ruby-version
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;chruby&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ~/.ruby-version
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;gem: --no-document&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.gemrc

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;gem update &lt;span class=&quot;nt&quot;&gt;--system&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;gem &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;bundler
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;bundle config &lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; without &lt;span class=&quot;s1&quot;&gt;&apos;development test&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The server is now ready to serve Ruby applications. In the next couple of posts, we’ll explore two different deployment setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2a.&lt;/strong&gt; &lt;a href=&quot;/serving-ruby-apps-with-caddy-as-a-reverse-proxy&quot;&gt;Serving Ruby apps with Caddy as a reverse proxy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2b.&lt;/strong&gt; &lt;a href=&quot;/falcon-as-a-web-server-for-ruby-applications&quot;&gt;Falcon as a web server for Ruby applications&lt;/a&gt;&lt;/p&gt;</content><author><name>Ayush Newatia</name></author></entry></feed>