Deploy Ruby On Rails:
Ubuntu 20.04 Focal Fossa in 2024
The definitive guide to deploying Ruby on Rails to production to your Virtual Private Server (VPS).
Operating System
In this guide, we'll deploy Ruby on Rails using Kamal. Kamal is a deployment tool that uses Docker containers to deploy your application with free SSL certificates and zero downtime deployments.
First, we need a server to deploy our code to. A Virtual Private Server (VPS) is a common choice but you can also deploy to a bare metal server too.
Here are some hosts you could use. We're fans of DigitalOcean and Hetzner.
- DigitalOcean (Get $200 in free credit!)
- Hetzner
- Linode
- Vultr
- Any hosting provider that offers Ubuntu with root access like AWS, Google Cloud, or Azure.
We'll create an Ubuntu server on DigitalOcean in this example, but the process is the same for most hosting providers.
Create A Server
Let's start with Create -> Droplet on DigitalOcean's dashboard. Choose a region near you or your users.
Choose the latest Ubuntu LTS version as the OS. LTS versions get long-term support which is ideal for production.
Choose a size for your server. You'll want at least 1GB of RAM but this will depend on your application's needs.
Configuring Kamal
Kamal is a free, open source, zero-downtime deployment tool. New Rails applications come with Kamal pre-installed and configured for the dependencies you've chosen (like PostgreSQL).
If you have an older application, you can add Kamal to the Gemfile and initialize it with the following command.
bundle add kamal && kamal init
Kamal has 2 main files to configure:
config/deploy.yml
.kamal/secrets
The deploy yaml file contains the server IP, roles, domain, and other details for where to deploy your application. The secrets file contains secrets (like API tokens and passwords) like your Docker registry token and Rails master key needed for deployment.
Docker Registry
We'll need a Docker registry to store the image for our production application. We'll use Docker Hub for this.
Login to Docker Hub and create a Personal Access Token with Read & Write permissions.
Once created, save this key somewhere secure. Then we will add it to our terminal to use with Kamal.
export KAMAL_REGISTRY_PASSWORD=my-secret-token
Let's update config/deploy.yml
with our Docker Hub username and server IP address.
Change my_app
to the name of your application, my-user
to your Docker Hub username and 192.168.0.1
to your server's IP. Some of these are used in a couple places, so make sure to update all of them.
# Name of your application. Used to uniquely configure containers.
service: my_app
# Name of the container image.
image: your-user/my_app
# Deploy to these servers.
servers:
web:
- 192.168.0.1
# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).
proxy:
ssl: true
host: app.example.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: your-user
# Always use an access token rather than real password when possible.
password:
- KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
secret:
- RAILS_MASTER_KEY
clear:
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: true
# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
console: app exec --interactive --reuse "bin/rails console"
shell: app exec --interactive --reuse "bash"
logs: app logs -f
dbc: app exec --interactive --reuse "bin/rails dbconsole"
# 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:
- "my_app_storage:/rails/storage"
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets
# Configure the image builder.
builder:
arch: amd64
Adding Database Servers
The above is the bare bones configuration for Kamal to deploy Rails with SQLite as the database. Your SQLite database will be stored in storage
which is mounted as a volume on the host OS. This preserves your database across deploys.
For other databases like PostgreSQL, MySQL or Redis, you will want to add accessories for each service. These services will be run as separate Docker containers and can run on the same or separate machines.
Here are some example accessories for PostgreSQL, MySQL, and Redis.
# Use accessory services (secrets come from .kamal/secrets).
accessories:
postgres:
image: postgres:17
host: 192.168.0.1
port: 5432:5432
env:
clear:
POSTGRES_USER: postgres
POSTGRES_DB: my_app_production
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
mysql:
image: mysql:8
host: 192.168.0.1
# Change to 3306 to expose port to the world instead of just local network.
port: "127.0.0.1:3306:3306"
env:
clear:
MYSQL_ROOT_HOST: '%'
secret:
- MYSQL_ROOT_PASSWORD
files:
- config/mysql/production.cnf:/etc/mysql/my.cnf
- db/production.sql:/docker-entrypoint-initdb.d/setup.sql
directories:
- data:/var/lib/mysql
redis:
image: redis:7
host: 192.168.0.1
port: 6379
directories:
- data:/data
Deploy your Rails application
Now that we've told Kamal how to access Docker Hub and our server, it's time to deploy.
For our first run, we need to set up the server to get it ready for deployments and deploy our app for the first time.
kamal setup
Open your server's IP address (or domain) in your browser to view your newly deployed app!
You can deploy new changes with the deploy command.
kamal deploy