Change Database User Password

To change the database password, run the ansible playbook provided in Change Database Password.

Define the Environment Variable

Setup Database Environment Variable
export CAPT_DATABASE_PASSWORD='new-password' 

The new-password is your new password used in the previous step.

Database Configuration

The config/database.yml file will pickup the password from this environment variable:

Database Configuration
  <<: *default
  database: capt_production
  username: deploy 
  password: <%= ENV["CAPT_DATABASE_PASSWORD"] %>

The database user is deploy. The Packer built the image with the deploy database user has create database permission. When Capistrano runs as deploy user, it will be able to run rails db:create. Do not change the username for production. You can change the default password for the deploy database user as explained above.

Add Capistrano related gems to the development group in the Gemfile:

group :development do
  gem 'capistrano', require: false
  gem 'capistrano-rails', require: false
  gem 'capistrano-bundler', require: false
  gem 'ed25519', '>= 1.2', '< 2.0'
  gem 'bcrypt_pbkdf', '>= 1.0', '< 2.0'

I am not using the capistrano3-puma gem because it is not compatible with Puma 6.4.2. The gem has issues due to breaking changes related to daemonizing the Puma process. Instead, we will be using a Puma playbook for running and restarting Puma.

The config/deploy.rb in the demo Rails project has Puma restart capistrano task that will automatically restart on deploy.

Capify Project

Run bundle install. You can now capify your Rails project by running:

Capify Command
cap install

Configure Capfile

The Capfile contents:

# Load DSL and set up stages
require "capistrano/setup"

# Include default deployment tasks
require "capistrano/deploy"

# Load the SCM plugin appropriate to your project:

require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git

# Include tasks from other gems included in your Gemfile

require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

Configure Deploy

Add these lines to config/deploy.rb:

Configure Capistrano
# config valid for current version and patch releases of Capistrano
lock "~> 3.18.1"

server '', user: 'deploy', roles: %w{app db web}, port: 2222
set :application, "capt"
set :branch, 'main'
set :repo_url, ""
set :deploy_to, '/home/deploy/apps/capt'
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/master.key')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system', 'public/uploads')
set :keep_releases, 5

set :default_env, {
  • Replace with your Rails server static IP address.
  • Change capt for application to your application name.
  • Change the repo_url to your project repo url.
  • Replace your-app-name in deploy_to to your Rails app name.

Do not change /home/deploy/apps/. The /home/deploy/apps directory is used because the prebuilt Packer image has already created it with the proper permissions for the deploy user. This allows Capistrano to create all the necessary directories under your Rails app name folder. Otherwise Capistrano deploy will fail due to directory permission issues.

Define Custom Tasks

Copy the following to the end of config/deploy.rb:

namespace :deploy do
  namespace :check do
    before :linked_files, :upload_config_files do
      on roles(:app), in: :sequence, wait: 10 do
        # Upload master.key
        unless test("[ -f #{shared_path}/config/master.key ]")
          upload! 'config/master.key', "#{shared_path}/config/master.key"

        # Upload database.yml
        unless test("[ -f #{shared_path}/config/database.yml ]")
          upload! 'config/database.yml', "#{shared_path}/config/database.yml"

namespace :deploy do
  desc 'Create database if it does not exist'
  task :create_db do
    on roles(:db) do
      within release_path do
        with rails_env: fetch(:rails_env) do
          execute :rails, 'db:create'

  before 'deploy:migrate', 'deploy:create_db'

# Capistrano task to print environment variables
namespace :deploy do
  desc 'Print environment variables'
  task :print_env do
    on roles(:app), in: :sequence, wait: 5 do
      execute :printenv

  after 'deploy:published', 'deploy:print_env'

The first time deploy creates the production database. We also print the environment variables to verify that they are set correctly on the server by Capistrano. Otherwise, you will run into database connection issues due to missing database password. We also upload the master.key and database.yml that will be retained across deployments. Capistrano will automatically run migrations when code is deployed.

Establish SSH Connection

Copy ~/.ssh/ key file on your development machine to the Rails server ~/.ssh/authorized_keys.

Copy Public Key on Dev Machine
cat ~/.ssh/

Copy the output and save it on the server:

Save the Public Key on Server
vi ~/.ssh/authorized_keys

Now you can test the SSH connection on the development machine by running:

ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@

Replace the IP address with your EC2 static IP address. By default the Packer image uses the port 2222 for SSH connection. If this works, the key is setup correctly and Capistrano will be able to deploy your Rails app.

Configure Allowed Hosts

In config/environments/production.rb, add:

  config.hosts << ""
  config.hosts << ""  # If you also want to explicitly allow requests to www subdomain

Replace with your domain name.

Run Deploy Command

On the development machine, run:

Cap Deploy Command
cap production deploy

Create Inventory File

In the project, create inventory.ini inside ansible folder:

Ansible Inventory File

ansible_ssh_common_args='-o StrictHostKeyChecking=no'

Run Puma Playbook

Run puma playbook in the ansible/playbooks/deploy directory. Replace the capt with the name of your app in the command line. In the ansible directory, run:

Run Puma Playbook
ansible-playbook -i inventory.ini ./playbooks/deploy/puma.yml -e "app_name=capt"

Map IP to domain

This is covered in Mapping IP to Domain

Run SSL Playbook

Run ssl playbook in the ansible/playbooks/deploy directory.

Run SSL Playbook
ansible-playbook -i inventory.ini ./playbooks/deploy/ssl.yml

Verify Running App

Browse to the health check endpoint: It should display a green background.

The restart-puma playbook in the ansible/playbooks/deploy directory is handy if you make any changes to Puma config.