DEV Community

Cover image for Deploy Rails apps for $5/month
Samuel G.
Samuel G.

Posted on

Deploy Rails apps for $5/month

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 5USD/month on Vultr.com 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.

In this guide we will be building a lightweight, reproducible setup using a

  • a single VPS on vultr.com
  • Ubuntu
  • Postgres for database
  • Kamal for deployment automation

🌐 1. Buying & Configuring the Domain (Namecheap)

I usually first pick a domain and buy it from providers like Namecheap for example, and configure the DNS to point to the new VPS that'll host the app:

Add DNS Records

Once my Vultr VPS is created which we'll do in the next step, we can point the domain to it by adding:

Type Host Value TTL
A @ <your-server-ip> Automatic
CNAME Record www <you-domain-name> Automatic

DNS propagation typically takes a few minutes.

🖥️ 2. Setting Up the Ubuntu Server (Vultr.com)

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

By default you will have an SSH access to the root account, so once the initial provisioning is complete SSH into the server.

Initial System Setup

Do the following to make sure the system is updated.

sudo apt update && sudo apt upgrade -y
Enter fullscreen mode Exit fullscreen mode

Optionally you can generate an ssh-key locally on your machine and add your public key it to the remote server to enable ssh login through your key.
On your machine do:

ssh-keygen -t ed25519 -C your@email.com
ssh-copy-id -i ~/.ssh/name_of_your_ssh_file.pub root@vps-ip
Enter fullscreen mode Exit fullscreen mode

📁 3. Preparing a Local Storage Directory for Kamal

Kamal supports mapping directories from the host machine into containers. We can created a directory that will hold uploads and persistent app data:

sudo mkdir -p /var/lib/app_name/storage
sudo chown $USER:$USER /var/lib/app_name/storage
Enter fullscreen mode Exit fullscreen mode

Then, in deploy.yml:

volumes:
  - /var/lib/app_name/storage:/app/storage
Enter fullscreen mode Exit fullscreen mode

This gives you persistent storage across deployments.

🐘 4. Installing PostgreSQL

Install Postgres and contrib packages:

sudo apt install postgresql postgresql-contrib -y
Enter fullscreen mode Exit fullscreen mode

Create a Postgres Superuser

sudo -u postgres createuser root -s
Enter fullscreen mode Exit fullscreen mode

Set the postgres user password interactively:

sudo -u postgres psql
postgres=# \password root
Enter fullscreen mode Exit fullscreen mode

🔗 5. Configuring Postgres for External (Docker) Connections

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:

postgresql.conf

# sudo nano /etc/postgresql/<your-version>/main/postgresql.conf
sudo nano /etc/postgresql/16/main/postgresql.conf
Enter fullscreen mode Exit fullscreen mode

Find the line

# listen_addresses = 'localhost'  # what IP address(es) to listen on;
Enter fullscreen mode Exit fullscreen mode

and edit it to read like the following, then save it and quit.

listen_addresses = '*'  # what IP address(es) to listen on;
Enter fullscreen mode Exit fullscreen mode

pg_hba.conf

# sudo nano /etc/postgresql/<your-version>/main/pg_hba.conf
sudo nano /etc/postgresql/16/main/pg_hba.conf
Enter fullscreen mode Exit fullscreen mode

Find the line

# IPv4 local connections:
host    all    all    127.0.0.1/32    scram-sha-256
Enter fullscreen mode Exit fullscreen mode

and edit it to read like the following, then save it and quit.

# IPv4 local connections:
host    all    all    172.16.0.0/12   scram-sha-256
Enter fullscreen mode Exit fullscreen mode

Then restart the postgresql service for changes to take effect:

sudo systemctl restart postgresql
Enter fullscreen mode Exit fullscreen mode

🔥 6. Configuring the Firewall (UFW)

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.

Allow SSH

This will most likely be done by default so you mostly won't need to do this.

sudo ufw allow OpenSSH
Enter fullscreen mode Exit fullscreen mode

Allow Postgres for Docker Subnet

sudo ufw allow from 172.16.0.0/12 to any port 5432 proto tcp comment 'Allow Docker containers to connect to Host PostgreSQL'
Enter fullscreen mode Exit fullscreen mode

Allow HTTPS (443)

sudo ufw allow 443/tcp comment 'Allow all incoming HTTPS traffic'
Enter fullscreen mode Exit fullscreen mode

Enable UFW

sudo ufw enable
Enter fullscreen mode Exit fullscreen mode

🗄️ 7. Configure your Rails database.yml file

Now that almost everything is set up, we can tell Rails which database to use.

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 host: <%= Rails.application.credentials.dig(:database, :host)%> but we'll leave that for your preffered practice.

production:
  primary: &primary_production
    <<: *default
    host: 172.17.0.1
    port: 5432
    username: root
    password: root
    database: app_name_production
  cache:
    <<: *primary_production
    database: app_name_production_cache
    migrations_paths: db/cache_migrate
  queue:
    <<: *primary_production
    database: app_name_production_queue
    migrations_paths: db/queue_migrate
  cable:
    <<: *primary_production
    database: app_name_production_cable
    migrations_paths: db/cable_migrate
Enter fullscreen mode Exit fullscreen mode

🚢 8. Deploying with Kamal

In the deploy.yml file make sure to configure the following correctly.

service: app_name

# Name of the container image.
image: dockerhub_username/app_name

# Deploy to these servers.
servers:
  web:
    - vps_ip_address
ssh:
  # You can also specify the path to your SSH key file:
  keys:
    - "~/.ssh/ssh_key_name"

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
proxy:
  ssl: true
  host: domain_name.com

# Credentials for your image host.
registry:
  # Specify the registry server, if you're not using Docker Hub
  # server: registry.digitalocean.com / ghcr.io / ...
  username: dockerhub_username

  # Always use an access token rather than real password when possible.
  password:
    - KAMAL_REGISTRY_PASSWORD

# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
  - "/var/lib/app_name/storage:/rails/storage"
Enter fullscreen mode Exit fullscreen mode

With the server ready, deploying the app with Kamal is simple:

kamal setup
Enter fullscreen mode Exit fullscreen mode

Note: The KAMAL_REGISTRY_PASSWORD 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:

KAMAL_REGISTRY_PASSWORD=dockerhub_token kamal setup
Enter fullscreen mode Exit fullscreen mode

Kamal will:

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

Subsequent deployment can be done by running

kamal deploy
Enter fullscreen mode Exit fullscreen mode

🏁 Conclusion

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.

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.

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👋!

Top comments (0)