Deploying a sinatra app in ruby with MRSK

May 25, 2023
MRSK is a new deployment tool recently unveiled by DHH. Giving it a try was long awaited.  So when my work needed me to deploy a small sinatra app with webhook that handles emailing our developers playbook. I decided to use MRSK and give it a try.
Mrsk is referred as capistrano for containers. And I loved capistrano, except I always considered it a little slow, combined with the problem that my shitty ISP has it does irritate me at times. 
I wanted a simple solution without any high availability architecture. Needed a linode so fired up one. I have been using linode for over a decade now and I have always loved it. Except since Akamai acquired it, notification is broken in my browser. 
❯ linode-cli linodes create \                                                                                                  ─╯
  --image 'linode/ubuntu22.04' \
  --region us-east \
  --type g6-nanode-1 \
  --label ubuntu-us-east \
  --tags rumrail \
  --root_pass XXXXXXXXXXXX \
  --authorized_users kapilnakhwa \
  --booted true \
  --backups_enabled false \
  --private_ip true
Give it a few seconds to provision. 
❯ linode-cli linodes list
┌──────────┬────────────────┬─────────┬───────────────┬────────────────────┬─────────┬─────────────────────────────────┐
│ id       │ label          │ region  │ type          │ image              │ status  │ ipv4                            │
├──────────┼────────────────┼─────────┼───────────────┼────────────────────┼─────────┼─────────────────────────────────┤
│ 38267970 │ ubuntu-ap-west │ ap-west │ g6-standard-1 │ linode/ubuntu22.04 │ running │ X.X.X.X, 192.168.142.62 │
│ 46428912 │ ubuntu-us-east │ us-east │ g6-nanode-1   │ linode/ubuntu22.04 │ running │ X.X.X.X, 192.168.201.96  │
└──────────┴────────────────┴─────────┴───────────────┴────────────────────┴─────────┴─────────────────────────────────┘
Don't you love working with 300ms ping. And my workplace has 3 guys looking after our technical infrastructure.
❯ ping X.X.X.X
PING X.X.X.X (143.42.121.221): 56 data bytes
64 bytes from X.X.X.X1: icmp_seq=0 ttl=49 time=305.579 ms
64 bytes from 1X.X.X.X: icmp_seq=1 ttl=49 time=330.765 ms
64 bytes from X.X.X.X: icmp_seq=2 ttl=49 time=345.785 ms
64 bytes from X.X.X.X: icmp_seq=3 ttl=49 time=369.387 ms
64 bytes from X.X.X.X: icmp_seq=4 ttl=49 time=397.382 ms
^X64 bytes from X.X.X.X: icmp_seq=5 ttl=49 time=310.272 ms
64 bytes from X.X.X.X: icmp_seq=6 ttl=49 time=333.802 ms
64 bytes from X.X.X.X: icmp_seq=7 ttl=49 time=279.226 ms
^C
--- X.X.X.X ping statistics ---
8 packets transmitted, 8 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 279.226/334.025/397.382/34.982 ms

Ranting apart, Let's start, Simple sinatra app racked up , picking up puma as webserver. Here's a look at file structure when finished. 
❯ tree .
.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── assets
│   └── developer_playbook.pdf
├── config
│   └── deploy.yml
├── config.ru
└── main.rb
Don't worry we'll get there. 
Let's create a gemfile for our project. 
bundle init
Add json, sinatra, mail , pumaand whatever our app might require.
❯ cat Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

# gem "rails"
gem 'sinatra'
gem 'mail'
gem 'dotenv'
gem 'puma'

First up main.rb file. 
❯ cat main.rb
require 'sinatra'
require 'mail'
require 'dotenv/load'
require 'json'

set :port, 80

Mail.defaults do
  delivery_method :smtp, {
    address: 'smtp.gmail.com',
    port: 587,
    domain: 'rumrail.com',
    user_name: ENV['GMAIL_USERNAME'] ,
    password: ENV['GMAIL_PASSWORD'],
    authentication: 'plain',
    enable_starttls_auto: true,
    read_timeout: 300
  }
end

class DootApp < Sinatra::Base
  post '/downloads/developers_playbook' do
    request_body = JSON.parse(request.body.read)
    recipient_email = request_body['Email']
    # Create a new email message
    mail = Mail.new do
      from    'kapil@XXXX.com'
      to      recipient_email
      subject 'Your Requested Developer Playbook'
      body    'Thank you for your interest in XXXXXX! Please find attached the Developer Playbook you requested.'
      add_file 'assets/developer_playbook.pdf'
    end
    # Send the email
    puts "sending mail"
    mail.deliver
    puts "Mail sent"
    # Return a success message
    content_type :json
    {message: 'success', status: 200}.to_json
  end

  get '/up' do
    status 200
  end
end

Note the "get /up" part Mrsk needs that, Mrsk deploys traefik as web server and uses /up as healthcheck by default for the containers. 

Let's build up a rack file. 
❯ cat config.ru
require './main'
run DootApp

Now we would need a DockerFile for the purpose of building image.

❯ cat Dockerfile
# Dockerfile
# Include the Ruby base image (https://hub.docker.com/_/ruby)
FROM ruby:3.2.1

# Install curl
RUN apt-get update \
    && apt-get install -y curl
# Put all this application's files in a directory called /app.
# This directory name is arbitrary and could be anything.
WORKDIR /app
COPY . /app

# Run this command. RUN can be used to run anything. In our
# case we're using it to install our dependencies.
RUN bundle install

# Tell Docker to listen on port 80.
EXPOSE 80

# Tell Docker that when we run "docker run", we want it to
# run the following command:
# $ ruby main.rb
CMD ["bundle", "exec", "rackup", "-p", "80", "--host", "0.0.0.0"]

This Dockerfile would be used to build a container and push the image to dockerhub. 
I have a free account in dockerhub that should do for now, One private image. How miser. 

Oh before I start using mrsk, We need to install it 
❯ gem install mrsk
While doing it we also want to init our project from mrsk 
❯ mrsk init
There simple , This will create the required files i.e "config/deploy.yaml". We all love YAML don't we. 
The environment variables are used by .env.erb file and command to extract out .env from .env.erb file is 
❯ mrsk envify
Obviously I would not show you the contents here.  But you get the gist.  it will create out .env file will by environment variables needed.
Here's the copy of the deploy file. 
❯ cat config/deploy.yml
# Name of your application. Used to uniquely configure containers.
service: doot

# Name of the container image.
image: kapilnakhwa/doot

# Deploy to these servers.
servers:
  - rumrailserver.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: kapilnakhwa

  # Always use an access token rather than real password when possible.
  password:
    - MRSK_REGISTRY_PASSWORD
# Configure builder setup.

builder:
  multi-arch: false
# Configure a custom healthcheck (default is /up on port 3000)
healthcheck:
  path: /up
  port: 80
There you can notice the server name, and registry username and password. 
MRSK_REGISTRY_PASSWORD is fed through the .env file we built earlier. and I am still using X86 arch so no need for those arm compatible architectures. hence, the build is not configured to support multiple architectures. 
for registry user and password, you can use your dockerhub username and password.

To setup your server.
❯ mrsk setup
Acquiring the deploy lock
  INFO [a13e4c6b] Running /usr/bin/env mkdir mrsk_lock && echo "TG9ja2VkIGJ5OiBrYXBpbCBhdCAyMDIzLTA1LTI1VDA1OjIwOjU0WgpWZXJz
aW9uOiA3ZWZiYmQ1YTY0NTRkMzY2OWNjMTVjZmQ3NDgyN2YwMzBmNjljZjY4
Ck1lc3NhZ2U6IEF1dG9tYXRpYyBkZXBsb3kgbG9jaw==
" > mrsk_lock/details on rumrailserver.com
  INFO [a13e4c6b] Finished in 5.801 seconds with exit status 0 (successful).
  INFO [33dee2c2] Running docker -v on rumrailserver.com
  INFO [33dee2c2] Finished in 1.725 seconds with exit status 0 (successful).
Log into image registry...
  INFO [335089a9] Running docker login -u [REDACTED] -p [REDACTED] as kapilnakhwa@localhost

It builds a local image , pushed to registry then prepares the server and whole lot of stuff it does to my servers.  And yes you need your docker engine running when running this command. 
After the setup is done. You need to run following command. 
❯ mrsk deploy
That should be it, And yea if you get the authentication problem when pushing images.  fix up your ~/.docker/config.json . That is one gotcha for all using docker GUI .
 {
  1   "auths": {
  2     "https://index.docker.io/v1/": {
  3       "auth": "XXXXXXXXX="
  4     }
  5   },
  6   "currentContext": "desktop-linux"
  7 }
~

To check if yours apps are up . 
❯ mrsk app logs -f
  INFO Following logs on rumrailserver.com...
  INFO ssh -t root@rumrailserver.com 'docker ps --quiet --filter label=service=doot --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'
2023-05-25T05:25:24.469076661Z 127.0.0.1 - - [25/May/2023:05:25:24 +0000] "GET /up HTTP/1.1" 200 - 0.0015
2023-05-25T05:25:25.612593257Z 127.0.0.1 - - [25/May/2023:05:25:25 +0000] "GET /up HTTP/1.1" 200 - 0.0015
2023-05-25T05:25:26.716700401Z 127.0.0.1 - - [25/May/2023:05:25:26 +0000] "GET /up HTTP/1.1" 200 - 0.0019
2023-05-25T05:25:27.816517807Z 127.0.0.1 - - [25/May/2023:05:25:27 +0000] "GET /up HTTP/1.1" 200 - 0.0062
yeah , I told you up was important. 

Time to test the service. 
❯ curl -X POST -H 'Content-Type: application/json' -d '{"Email": "info@hikuro.com"}' http://rumrailserver.com/downloads/developers_playbook
{"message":"success","status":200}%
Simple, If you run into any problems feel free to contact me, If you need help in developing products for your business.