Friday 14th March, 2025
Using Kamal to deploy a Rails 8 app to Digital Ocean
These steps describe how to use Kamal to deploy a fresh Rails 8 app to a Digital Ocean Droplet.
Install and configure Digital Ocean CLI
Follow the Digital Ocean instructions to install doctl and use an API token to connect it to your account.
Create a Digital Ocean Container Registry
Kamal expects to be able to push/pull container images to/from a registry during deployment so we use doctl to create a private Container Registry in Digital Ocean.
Each Digital Ocean account can only create one Container Registry so I suggest something fairly generic (i.e. not specific to this application).
# Save the registry name in an environment variable so that we can use it later.
$ export REGISTRY_NAME=<registry-name>
# The ams3 region is Amsterdam because it's close to me.
# Use whichever region makes sense to you.
$ doctl registry create ${REGISTRY_NAME} --region ams3
Name               Endpoint                                     Region slug
<registry-name>    registry.digitalocean.com/<registry-name>    ams3
Create a Digital Ocean Personal Access Token for accessing the Container Registry
This allows Kamal to push/pull images from the Digital Ocean Container Registry created in the previous step.
Visit Digital Ocean API Tokens and create a new token with the following attributes:
- Name: container-registry-token
- Expiration: 30 days
- Scopes: Custom Scopes
- Custom scopes: registry update / read
Save your Digital Ocean username and the API key in the environment as we need these to configure Kamal.
$ export DO_USERNAME=<digital-ocean-username>
$ export KAMAL_REGISTRY_PASSWORD=<token>
Test that you can login to the registry using the token created in the previous step
$ echo ${KAMAL_REGISTRY_PASSWORD} | docker login --username "${DO_USERNAME}" --password-stdin registry.digitalocean.com
WARNING! Your password will be stored unencrypted in ~/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credential-stores
Login Succeeded
Add SSH key to Digital Ocean
This allows us to create a Droplet that’s configured for SSH Public Key authentication.
This assumes you have an existing SSH public key in ~/.ssh/id_ed25519.pub that hasn’t yet been added to your Digital Ocean account.
$ doctl compute ssh-key create <key-name> --public-key "$(cat ~/.ssh/id_ed25519.pub)"
ID          Name        FingerPrint
45068967    <key-name>  <ssh-key-fingerprint>
# Store the SSH key fingerprint so that we can use it when creating the Droplet
$ export SSH_KEY=<ssh-key-fingerprint>
Create a Digital Ocean Droplet
Use the Digital Ocean CLI to create a small ($6/month) Droplet.
Add an entry to /etc/hosts to avoid having to edit DNS at this stage.
$ export DROPLET_NAME=rails-app
$ doctl compute droplet create \
	--image ubuntu-24-10-x64 \
	--size s-1vcpu-1gb \
	--region lon1 \
	--ssh-keys ${SSH_KEY} \
	--wait \
	${DROPLET_NAME}
ID           Name               Public IPv4       Private IPv4    Public IPv6    Memory    VCPUs    Disk    Region    Image               VPC UUID                                Status    Tags    Features                            Volumes
<id>         rails-app          <public-ip>       <ip>                           1024      1        25      lon1      Ubuntu 24.10 x64    <uuid>                                  active            droplet_agent,private_networking
# Store the public IP in an environment variable
$ export PUBLIC_IP=<public-ip>
# Store the hostname in an environment variable
$ export HOST_NAME=rails-app.example
# Edit /etc/hosts to allow named access to the server
$ echo "${PUBLIC_IP} ${HOST_NAME}" | sudo tee -a /etc/hosts
# SSH to the machine
$ ssh root@${HOST_NAME}
# Configure firewall to block everything except SSH, HTTP and HTTPS traffic
$ ufw allow ssh
$ ufw allow http
$ ufw allow https
$ ufw enable
# Check status of firewall
$ ufw status
Status: active
To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443                        ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)
Create a new Rails app
# Create new Rails project
$ rails new rails-app
$ cd rails-app
# Add to git
$ git add .
$ git commit -m 'Create empty Rails 8 app'
Update config/deploy.rb
By applying the following changes:
--- a/config/deploy.yml
+++ b/config/deploy.yml
@@ -2,12 +2,12 @@
 service: rails_app
 # Name of the container image.
-image: your-user/rails_app
+image: <%= ENV['REGISTRY_NAME'] %>/rails_app
 # Deploy to these servers.
 servers:
   web:
-    - 192.168.0.1
+    - <%= ENV['HOST_NAME'] %>
   # job:
   #   hosts:
   #     - 192.168.0.1
@@ -18,14 +18,15 @@ servers:
 #
 # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
 proxy:
-  ssl: true
-  host: app.example.com
+  ssl: false
+  host: <%= ENV['HOST_NAME'] %>
 # 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
+  server: registry.digitalocean.com
+  username: <%= ENV['DO_USERNAME'] %>
   # Always use an access token rather than real password when possible.
   password:
Use Kamal to setup the server and deploy the app
$ kamal setup
Test the deployment
Use curl to check that the app has been deployed successfully.
$ curl http://${HOST_NAME}/up
<!DOCTYPE html><html><body style="background-color: green"></body></html>
Enable SSL
This requires a public DNS entry for your Rails app so that Let’s Encrypt can issue a certificate.
- Create a new DNS entry for your server and point it at the public IP of the Digital Ocean Droplet created earlier
- Update the HOST_NAMEenvironment variable with the new host
- Replace ssl: falsewithssl: trueunderproxyin config/deploy.rb
- Run kamal deployto enable SSL in the proxy
- Check that SSL is working with curl:
$ curl -v http://${HOST_NAME}
< HTTP/1.1 301 Moved Permanently
< Location: https://<host-name>/
$ curl https://${HOST_NAME}/up
<!DOCTYPE html><html><body style="background-color: green"></body></html>
Tidying up
Remove everything created in the steps above:
- Delete Digital Ocean Droplet
    - $ doctl compute droplet delete ${DROPLET_NAME}
 
- Delete SSH Key from Digital Ocean
    - $ doctl compute ssh-key delete ${SSH_KEY}
 
- Delete Digital Ocean Container Registry
    - $doctl registry delete
 
- Remove entry from /etc/hosts
    - $ sudo sed -e "s/${PUBLIC_IP} ${HOST_NAME}//g" -i /etc/hosts
 
- Delete Personal Access Token
- Delete DNS entry if required
Tools used
The above instructions were tested as working with the following tool versions.
$ lsb_release -d
No LSB modules are available.
Description:	Ubuntu 24.04.1 LTS
$ doctl version
doctl version 1.120.2-release
Git commit hash: 34e9fbbe
$ ruby --version
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [x86_64-linux]
$ rails --version
Rails 8.0.1
$ docker --version
Docker version 27.5.0, build a187fa5
$ kamal version
2.4.0
 
            
If you have any feedback on this article, please get in touch!
Historical comments can be found here.