This is a simple Rails 7.1 application built based on Rails "blog app" guide. We will be using this app to learn how to deploy a Rails app to VPS with Kamal.
Sources:
Pre-requisites:
- Ruby 3.3.4
- installed and configured git
- installed and configured ssh
- ssh-key without passphrase
- docker installed and updated
git clone [email protected]:visualitypl/kamal-workshops-app.git
git clone https://github.com/visualitypl/kamal-workshops-app.git
gem install kamal
Example files for this task:
kamal init
We will need to edit:
- service (name of the app: blog-space)
- image (#{Docker Hub username from the companion app}/#{name of the app})
- servers (IP from the companion app)
- registry (username: #{Docker Hub username from the companion app})
- env (clear, secret)
- builder (remote: [arch: amd64, host: #{from the companion app}])
We will need to edit:
- KAMAL_REGISTRY_PASSWORD (Docker Hub token from the companion app)
- SECRET_KEY_BASE (generate secret key base with
rails secret
) - POSTGRES_PASSWORD (pick a password)
We will need to rename docker-setup.sample to docker-setup in .kamal/hooks/ directory
To prepare docker on hosts, push env, deploy postgres accessory and deploy the app we need to run:
kamal server bootstrap
kamal env push
kamal accessory boot postgres
kamal deploy
Alternatively, you can run command that calls all the above:
kamal setup
To check the result, run:
kamal audit
kamal details
Visit the app at http://#{IP}
Example files for this task:
Add "worker" role to servers (save IP as web)
servers:
worker:
hosts:
- 255.255.255.100 ## CHANGE ME
cmd: bundle exec sidekiq
options:
network: "kamal"
We will need to edit config/environments/production.rb
and config/environments/staging.rb
config.active_job.queue_adapter = :sidekiq
Add "redis" accessory
accessories:
redis:
image: "redis:7.2-alpine"
roles:
- web
directories:
- data:/data
options:
network: "kamal"
cmd: "redis-server --requirepass <%= File.read('.env')[/REDIS_PASSWORD="(.*?)"/, 1] %>"
Add two env vars to .env
:
- REDIS_PASSWORD (pick a password)
- REDIS_URL (
"redis://:<REDIS_PASSWORD>@blog-space-redis:6379/0"
, substitute the password with above)
Add REDIS_URL
to env
section of deploy.yml
env:
secret:
- SECRET_KEY_BASE
- POSTGRES_PASSWORD
- REDIS_URL
Edit config/cable.yml
and set adapter: redis
on production and staging
staging:
adapter: redis
url: <%= ENV.fetch("REDIS_URL", "") %>
channel_prefix: blog_space_production
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL", "") %>
channel_prefix: blog_space_production
git add -A
git commit -m "Add redis and sidekiq"
kamal env push
kamal accessory boot redis
kamal deploy
To check the result, run:
kamal audit
kamal details
Visit the app at http://#{IP}
Example files for this task:
Create config/deploy.staging.yml
.
We only need to override values from config/deploy.yml
that are different for staging.
servers:
web:
hosts:
- [HOST IP] ## CHANGE ME
worker:
hosts:
- [HOST IP] ## CHANGE ME
env:
clear:
RAILS_ENV: "staging"
POSTGRES_DB: "blog_space_staging"
accessories:
postgres:
files:
- "config/init.staging.sql:/docker-entrypoint-initdb.d/init.sql"
redis:
cmd: "redis-server --requirepass <%= File.read('.env.staging')[/REDIS_PASSWORD="(.*?)"/, 1] %>"
Create .env.staging
and add:
- SECRET_KEY_BASE (generate secret key base with
rails secret
) - POSTGRES_PASSWORD (pick a password)
- REDIS_PASSWORD (pick a password)
- REDIS_URL (
"redis://:<REDIS_PASSWORD>@blog-space-redis:6379/0"
, substitute the password with above)
Notice that we are not setting KAMAL_REGISTRY_PASSWORD
env var, because we are using the same one as in production.
To use kamal with staging destination we need to pass -d staging
flag to all commands.
Like before we need to setup docker on hosts, push env, deploy postgres accessory and deploy the app.
kamal setup -d staging
kamal server bootstrap -d staging
kamal env push -d staging
kamal accessory boot all -d staging
kamal deploy -d staging
To check the result, run:
kamal audit -d staging
kamal details -d staging
Visit the app at http://#{STAGING-IP}
We will break the app by generating a migration that will fail.
bundle exec rails generate migration addAuthorToArticle
class AddAuthorToArticle < ActiveRecord::Migration[7.1]
def change
add_column :articles, :author, :string, null: false
end
end
Commit the changes and push them to the server.
git add -A
git commit -m "Add author to article"
kamal deploy
Inspect the output of the deploy command and notice that the app have not been deployed to the server.
Fix the migration by adding a default value to the author column. BUT at the same time break something else.
class AddAuthorToArticle < ActiveRecord::Migration[7.1]
def change
add_column :articles, :author, :string, null: false, default: ""
end
end
Comment the "delete_barons_comments" route in config/routes.rb
# post "delete_barons_comments", on: :member
Commit the changes and push them to the server.
This time the migration were applied to the database. But "the core functionality" of the app is lost.
Rollback to previous version of the app:
kamal rollback <VERSION>
# to find the version run:
kamal audit
Remove the migration that we added previously.
This task has no solution here ;)