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
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
📁 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
Then, in deploy.yml:
volumes:
- /var/lib/app_name/storage:/app/storage
This gives you persistent storage across deployments.
🐘 4. Installing PostgreSQL
Install Postgres and contrib packages:
sudo apt install postgresql postgresql-contrib -y
Create a Postgres Superuser
sudo -u postgres createuser root -s
Set the postgres user password interactively:
sudo -u postgres psql
postgres=# \password root
🔗 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
Find the line
# listen_addresses = 'localhost' # what IP address(es) to listen on;
and edit it to read like the following, then save it and quit.
listen_addresses = '*' # what IP address(es) to listen on;
pg_hba.conf
# sudo nano /etc/postgresql/<your-version>/main/pg_hba.conf
sudo nano /etc/postgresql/16/main/pg_hba.conf
Find the line
# IPv4 local connections:
host all all 127.0.0.1/32 scram-sha-256
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
Then restart the postgresql service for changes to take effect:
sudo systemctl restart postgresql
🔥 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
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'
Allow HTTPS (443)
sudo ufw allow 443/tcp comment 'Allow all incoming HTTPS traffic'
Enable UFW
sudo ufw enable
🗄️ 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
🚢 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"
With the server ready, deploying the app with Kamal is simple:
kamal setup
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
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
🏁 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)