<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samuel G.</title>
    <description>The latest articles on DEV Community by Samuel G. (@samuelbruk).</description>
    <link>https://dev.to/samuelbruk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3088269%2F13660711-7e68-421c-b75b-45744d16357e.jpg</url>
      <title>DEV Community: Samuel G.</title>
      <link>https://dev.to/samuelbruk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samuelbruk"/>
    <language>en</language>
    <item>
      <title>Deploy Rails apps for $5/month</title>
      <dc:creator>Samuel G.</dc:creator>
      <pubDate>Sat, 15 Nov 2025 09:51:37 +0000</pubDate>
      <link>https://dev.to/samuelbruk/deploy-rails-apps-for-5month-13in</link>
      <guid>https://dev.to/samuelbruk/deploy-rails-apps-for-5month-13in</guid>
      <description>&lt;p&gt;When ever you want to deploy a small Ruby on Rails app, may be a blog or a personal website, you usually don't need anything more than a VPS that's running Ubuntu with 1Gb of memory, 25GB of storage and with SSH access, which you can get for about &lt;strong&gt;&lt;em&gt;5USD/month&lt;/em&gt;&lt;/strong&gt; on &lt;a href="https://www.vultr.com/?ref=9828154" rel="noopener noreferrer"&gt;&lt;em&gt;&lt;strong&gt;Vultr.com&lt;/strong&gt;&lt;/em&gt;&lt;/a&gt; and may be even cheaper on some other providers that I'm not aware of. Using this approach we can deploy the Rails app, host the storage and database for the app in the VPS, with out the need to provision separate VPSs or managed services for storage or database.&lt;/p&gt;

&lt;p&gt;In this guide we will be building a lightweight, reproducible setup using a&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a single VPS on &lt;a href="https://www.vultr.com/?ref=9828154" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;vultr.com&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ubuntu&lt;/li&gt;
&lt;li&gt;Postgres for database&lt;/li&gt;
&lt;li&gt;Kamal for deployment automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🌐 1. Buying &amp;amp; Configuring the Domain (Namecheap)
&lt;/h2&gt;

&lt;p&gt;I usually first pick a domain and buy it, from providers like &lt;a href="https://www.namecheap.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;Namecheap&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; for example, and configure the DNS to point to the new VPS that'll host the app:&lt;/p&gt;

&lt;h3&gt;
  
  
  Add DNS Records
&lt;/h3&gt;

&lt;p&gt;Once the VPS is created which we'll do in the next step, we can point the domain to it by adding:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;your-server-ip&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CNAME Record&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;you-domain-name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DNS propagation typically takes a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  🖥️ 2. Setting Up the Ubuntu Server (&lt;a href="https://www.vultr.com/?ref=9828154" rel="noopener noreferrer"&gt;Vultr.com&lt;/a&gt;)
&lt;/h2&gt;

&lt;p&gt;Create a &lt;strong&gt;VPS&lt;/strong&gt; with &lt;strong&gt;shared CPU&lt;/strong&gt;, &lt;strong&gt;1GB memory&lt;/strong&gt; and &lt;strong&gt;25GB SSD&lt;/strong&gt; running &lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt;. Currently this is &lt;strong&gt;5USD/month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By default you will have an SSH access to the root account, so once the initial provisioning is complete, SSH into the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial System Setup
&lt;/h3&gt;

&lt;p&gt;Do the following to make sure the system is updated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optionally you can generate an ssh-key locally on your machine and add your public key to the remote server to enable ssh login through your key.&lt;br&gt;
On your machine do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; your@email.com
ssh-copy-id &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/name_of_your_ssh_file.pub root@vps-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📁 3. Preparing a Local Storage Directory for Kamal
&lt;/h2&gt;

&lt;p&gt;Kamal supports mapping directories from the host machine into containers. We can created a directory that will hold uploads and persistent app data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/app_name/storage
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 1000:1000 /var/lib/app_name/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create the directory for storage and change the owner of the directory to be what the docker container will use, usually having a UID of 1000.&lt;/p&gt;

&lt;p&gt;Then, in &lt;code&gt;deploy.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/app_name/storage:/app/storage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you persistent storage across deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  🐘 4. Installing PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Install Postgres and contrib packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;postgresql postgresql-contrib &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a Postgres Superuser
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres createuser app_name_user &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the postgres user password interactively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql
&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# \password app_name_user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔗 5. Configuring Postgres for External (Docker) Connections
&lt;/h2&gt;

&lt;p&gt;Since Kamal uses docker to create the container hosting your app, the VPS's network will be accessible through a docker bridge to your app container. So we need to tell postgres to accept connections from that subnet which is the 172.16.0.0/12 subnet:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;postgresql.conf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sudo nano /etc/postgresql/&amp;lt;your-version&amp;gt;/main/postgresql.conf&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/postgresql/16/main/postgresql.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the line&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# listen_addresses = 'localhost'  # what IP address(es) to listen on;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and edit it to read like the following, then save it and quit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;listen_addresses&lt;/span&gt; = &lt;span class="s1"&gt;'*'&lt;/span&gt;  &lt;span class="c"&gt;# what IP address(es) to listen on;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;pg_hba.conf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sudo nano /etc/postgresql/&amp;lt;your-version&amp;gt;/main/pg_hba.conf&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/postgresql/16/main/pg_hba.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the line&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# IPv4 local connections:
&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;    &lt;span class="n"&gt;all&lt;/span&gt;    &lt;span class="n"&gt;all&lt;/span&gt;    &lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;/&lt;span class="m"&gt;32&lt;/span&gt;    &lt;span class="n"&gt;scram&lt;/span&gt;-&lt;span class="n"&gt;sha&lt;/span&gt;-&lt;span class="m"&gt;256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and edit it to read like the following, then save it and quit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# IPv4 local connections:
&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;    &lt;span class="n"&gt;all&lt;/span&gt;    &lt;span class="n"&gt;all&lt;/span&gt;    &lt;span class="m"&gt;172&lt;/span&gt;.&lt;span class="m"&gt;16&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;12&lt;/span&gt;   &lt;span class="n"&gt;scram&lt;/span&gt;-&lt;span class="n"&gt;sha&lt;/span&gt;-&lt;span class="m"&gt;256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart the postgresql service for changes to take effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔥 6. Configuring the Firewall (UFW)
&lt;/h2&gt;

&lt;p&gt;Its important to setup a firewall to prevent unwanted access and allow only the traffic we want. Fortunately we have UFW that comes preinstalled with the Ubuntu on our VPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Allow SSH
&lt;/h3&gt;

&lt;p&gt;This will most likely be done by default so you mostly won't need to do this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow OpenSSH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Allow Postgres for Docker Subnet
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 172.16.0.0/12 to any port 5432 proto tcp comment &lt;span class="s1"&gt;'Allow Docker containers to connect to Host PostgreSQL'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Allow HTTPS (443)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp comment &lt;span class="s1"&gt;'Allow all incoming HTTPS traffic'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable UFW
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🗄️ 7. Configure your Rails database.yml file
&lt;/h2&gt;

&lt;p&gt;Now that almost everything is set up, we can tell Rails which database to use.&lt;/p&gt;

&lt;p&gt;In the database.yml file configure the details to resolve to something like the following. Note: you should probably hide these details in the credentials Rails provides so you can access them via something like this &lt;code&gt;host: &amp;lt;%= Rails.application.credentials.dig(:database, :host)%&amp;gt;&lt;/code&gt; but we'll leave that for your preffered practice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;primary_production&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;172.17.0.1&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_name_user&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_name_user_password&lt;/span&gt;
    &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_name_production&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*primary_production&lt;/span&gt;
    &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_name_production_cache&lt;/span&gt;
    &lt;span class="na"&gt;migrations_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db/cache_migrate&lt;/span&gt;
  &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*primary_production&lt;/span&gt;
    &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_name_production_queue&lt;/span&gt;
    &lt;span class="na"&gt;migrations_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db/queue_migrate&lt;/span&gt;
  &lt;span class="na"&gt;cable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*primary_production&lt;/span&gt;
    &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_name_production_cable&lt;/span&gt;
    &lt;span class="na"&gt;migrations_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db/cable_migrate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚢 8. Deploying with Kamal
&lt;/h2&gt;

&lt;p&gt;In the deploy.yml file make sure to configure the following correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_name&lt;/span&gt;

&lt;span class="c1"&gt;# Name of the container image.&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dockerhub_username/app_name&lt;/span&gt;

&lt;span class="c1"&gt;# Deploy to these servers.&lt;/span&gt;
&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vps_ip_address&lt;/span&gt;
&lt;span class="na"&gt;ssh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# You can also specify the path to your SSH key file:&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/.ssh/ssh_key_name"&lt;/span&gt;

&lt;span class="c1"&gt;# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.&lt;/span&gt;
&lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;domain_name.com&lt;/span&gt;

&lt;span class="c1"&gt;# Credentials for your image host.&lt;/span&gt;
&lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Specify the registry server, if you're not using Docker Hub&lt;/span&gt;
  &lt;span class="c1"&gt;# server: registry.digitalocean.com / ghcr.io / ...&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dockerhub_username&lt;/span&gt;

  &lt;span class="c1"&gt;# Always use an access token rather than real password when possible.&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KAMAL_REGISTRY_PASSWORD&lt;/span&gt;

&lt;span class="c1"&gt;# Use a persistent storage volume for sqlite database files and local Active Storage files.&lt;/span&gt;
&lt;span class="c1"&gt;# Recommended to change this to a mounted volume path that is backed up off server.&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/lib/app_name/storage:/rails/storage"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the server ready, deploying the app with Kamal is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kamal setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: The &lt;code&gt;KAMAL_REGISTRY_PASSWORD&lt;/code&gt; is your Dockerhub or the repository of your choice's access token which should probably be stored in a password management service like 1PASSWORD, but for a quick deploy you can feed that directly to Kamal during deployment like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KAMAL_REGISTRY_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dockerhub_token kamal setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kamal will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install Docker on all servers, if it has permission and it is not already installed.&lt;/li&gt;
&lt;li&gt;boot all accessories.&lt;/li&gt;
&lt;li&gt;log in to the Docker registry locally and on all servers.&lt;/li&gt;
&lt;li&gt;build the app image, push it to the registry, and pull it onto the servers.&lt;/li&gt;
&lt;li&gt;ensure kamal-proxy is running and accepting traffic on ports 80 and 443.&lt;/li&gt;
&lt;li&gt;start a new container with the version of the app that matches the current Git version hash.&lt;/li&gt;
&lt;li&gt;tell kamal-proxy to route traffic to the new container once it is responding with 200 OK to GET /up on port 80.&lt;/li&gt;
&lt;li&gt;stop the old container running the previous version of the app.&lt;/li&gt;
&lt;li&gt;prune unused images and stopped containers to ensure servers don’t fill up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subsequent deployment can be done by running&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kamal deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🏁 Conclusion
&lt;/h2&gt;

&lt;p&gt;This setup is intentionally lightweight and cheap: just a VPS, Ubuntu, Postgres, UFW, and Kamal, perfect for a small app. It gives you full control while remaining simple enough to maintain.&lt;/p&gt;

&lt;p&gt;If you're building your own project or personal site and want a minimal deployment setup that stays out of your way, this workflow strikes a great balance between control and simplicity.&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts. Please leave a comment if you found this helpful or if you have any questions. I'll see you in another post👋!&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Committing `.DS_Store` Files to Git</title>
      <dc:creator>Samuel G.</dc:creator>
      <pubDate>Fri, 25 Apr 2025 19:24:45 +0000</pubDate>
      <link>https://dev.to/samuelbruk/stop-committing-dsstore-files-to-git-4op1</link>
      <guid>https://dev.to/samuelbruk/stop-committing-dsstore-files-to-git-4op1</guid>
      <description>&lt;p&gt;If you've ever worked with Git on macOS, chances are you’ve seen the mysterious &lt;code&gt;.DS_Store&lt;/code&gt; file show up in your repositories.&lt;/p&gt;

&lt;h4&gt;
  
  
  What is &lt;code&gt;.DS_Store&lt;/code&gt;?
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;.DS_Store&lt;/code&gt; is a hidden file that macOS creates in every folder to store attributes like icon positions, window size and other visual information. It is one of those things that seems to sneak in when you’re not looking.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why You Should Ignore It
&lt;/h4&gt;

&lt;p&gt;There’s absolutely no reason to track &lt;code&gt;.DS_Store&lt;/code&gt; in Git. It creates unnecessary merge conflicts, and adds noise to pull requests. Plus, it’s platform specific and your teammates on Linux or Windows won’t care about how you like your icons arranged.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to Ignore It
&lt;/h4&gt;

&lt;p&gt;The fix is simple. Add this lines to your project's &lt;code&gt;.gitignore&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.DS_Store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to make sure it never ends up in &lt;strong&gt;any&lt;/strong&gt; repo on your system again, you can add it globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; core.excludesfile ~/.gitignore_global
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your &lt;code&gt;~/.gitignore_global&lt;/code&gt; file, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.DS_Store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Cleaning Up
&lt;/h4&gt;

&lt;p&gt;If it's already staged you can unstage and remove the path from index as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="s1"&gt;'*.DS_Store'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. You’ve now taught Git to look the other way when it sees &lt;code&gt;.DS_Store&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>macos</category>
    </item>
  </channel>
</rss>
