Codementor Events

Build a JRuby on Rails application from scratch and Dockerize it!

Published Feb 19, 2018Last updated Aug 18, 2018
Build a JRuby on Rails application from scratch and Dockerize it!

About me

I'm a Ruby developer with several years of experience building and maintaining Ruby applications.

The problem I wanted to solve

I was reading the book "Deploying with JRuby 9k" and by experimenting with it, turns out the book is completely outdated. But I was able to pick some of the stuffs that I knew will be needed to accomplish my objective: Creating a JRuby on Rails and be able to Dockerize it.

My JRuby on Rails application

So, I built a JRuby on Rails application, a very basic but functional application that simply renders the default Rails welcome page. This application can be executed in Docker which is great since you will be able to deploy it on different clouds like DigitalOcean or EC2 or anything like that.

Tech stack

I used the following libraries/tools: JRuby version 9.1.15.0 (2.3.3), Rails 5.0.6, Docker 18.02.0-ce, Docker Toolbox, Postgresql 9, Redis 3.2.8, Java 8

The process of building JRuby on Rails application from scratch

First you gotta make sure you have Java 8 installed in your computer, then install JRuby, and generate the new Rails application rails new app_name_here.

Now you have the Rails application but it still needs a lot of changes. Lets begin by changing the Gemfile, replace gem 'activerecord-jdbcsqlite3-adapter' with ´gem 'activerecord-jdbcpostgresql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: '50-stable'´ because we will be using Postgresql. Add this gem gem 'rack-timeout', '~> 0.4'. Next add this line ruby '2.3.3', engine: 'jruby', engine_version: '9.1.15.0' after this block

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end

This indicates that you want to use JRuby instead of Ruby.
You will also need to add gem 'dotenv-rails' in your development group.

Lets move on, and create the .env file in the application root, then put the following inside it:

# This is used by Docker Compose to set up prefix names for Docker images,
# containers, volumes and networks. This ensures that everything is named
# consistently regardless of your folder structure.
COMPOSE_PROJECT_NAME=app_name_here

# What Rails environment are we in?
RAILS_ENV=development

# Rails log level.
#   Accepted values: debug, info, warn, error, fatal, or unknown
LOG_LEVEL=debug

# You would typically use `rails secret` to generate a secure token. It is
# critical that you keep this value private in production.
SECRET_TOKEN=asecuretokenwouldnormallygohere

# More details about these Puma variables can be found in config/puma.rb.
# Which address should the Puma app server bind to?
BIND_ON=0.0.0.0:3000

# Puma supports multiple threads but in development mode you'll want to use 1
# thread to ensure that you can properly debug your application.
RAILS_MAX_THREADS=1

# Puma supports multiple workers but you should stick to 1 worker in dev mode.
WEB_CONCURRENCY=1

# Requests that exceed 5 seconds will be terminated and dumped to a stacktrace.
# Feel free to modify this value to fit the needs of your project, but if you
# have any request that takes more than 5 seconds you probably need to re-think
# what you are doing 99.99% of the time.
REQUEST_TIMEOUT=30

# The database name will automatically get the Rails environment appended to it
# such as: app_name_here_development or app_name_here_production.
DATABASE_URL=postgresql://app_name_here:yourpassword@postgres:5432/app_name_here?encoding=utf8&pool=5&timeout=5000

# The full Redis URL for the Redis cache. The last segment is the namespace.
REDIS_CACHE_URL=redis://:yourpassword@redis:6379/0/cache

# Action mailer (e-mail) settings.
# You will need to enable less secure apps in your Google account if you plan
# to use GMail as your e-mail SMTP server.
# You can do that here: https://www.google.com/settings/security/lesssecureapps
SMTP_ADDRESS=smtp.gmail.com
SMTP_PORT=587
SMTP_DOMAIN=gmail.com
SMTP_USERNAME=you@gmail.com
SMTP_PASSWORD=yourpassword
SMTP_AUTH=plain
SMTP_ENABLE_STARTTLS_AUTO=true

# Not running Docker natively? Replace 'localhost' with your Docker Machine IP
# address, such as: 192.168.99.100:3000
ACTION_MAILER_HOST=192.168.99.100:3000
ACTION_MAILER_DEFAULT_FROM=you@gmail.com
ACTION_MAILER_DEFAULT_TO=you@gmail.com

# Google Analytics universal ID. You should only set this in non-development
# environments. You wouldn't want to track development mode requests in GA.
# GOOGLE_ANALYTICS_UA='xxx'

# The Redis URL.
REDIS_URL=redis://:yourpassword@redis:6379/0

# The full Redis URL for Active Job.
ACTIVE_JOB_URL=redis://:yourpassword@redis:6379/0

# The queue prefix for all Active Jobs. The Rails environment will
# automatically be added to this value.
ACTIVE_JOB_QUEUE_PREFIX=app_name_here:jobs

# The full Redis URL for Action Cable's back-end.
ACTION_CABLE_BACKEND_URL=redis://:yourpassword@redis:6379/0

# The full WebSocket URL for Action Cable's front-end.
# Not running Docker natively? Replace 'localhost' with your Docker Machine IP
# address, such as: ws://192.168.99.100:28080
ACTION_CABLE_FRONTEND_URL=ws://192.168.99.100:28080

# Comma separated list of RegExp origins to allow connections from.
# These values will be converted into a proper RegExp, so omit the / /.
#
# Examples:
#   http:\/\/localhost*
#   http:\/\/example.*,https:\/\/example.*
#
# Not running Docker natively? Replace 'localhost' with your Docker Machine IP
# address, such as: http:\/\/192.168.99.100*
ACTION_CABLE_ALLOWED_REQUEST_ORIGINS=http:\/\/192.168.99.100*

Docker will read all this environment variables from this file.

Next, lets create the docker-compose.yml

version: '2'

services:
  postgres:
    image: 'postgres:9.6-alpine'
    environment:
      POSTGRES_USER: 'app_name_here'
      POSTGRES_PASSWORD: 'yourpassword'
    ports:
      - '5432:5432'
    volumes:
      - 'postgres:/var/lib/postgresql/data'

  redis:
    image: 'redis:3.2-alpine'
    command: redis-server --requirepass yourpassword
    ports:
      - '6379:6379'
    volumes:
      - 'redis:/data'

  website:
    depends_on:
      - 'postgres'
      - 'redis'
    build: .
    ports:
      - '3000:3000'
    volumes:
      - '.:/app'
    env_file:
      - '.env'

  sidekiq:
    depends_on:
      - 'postgres'
      - 'redis'
    build: .
    command: sidekiq -C config/sidekiq.yml.erb
    volumes:
      - '.:/app'
    env_file:
      - '.env'

  cable:
    depends_on:
      - 'redis'
    build: .
    command: puma -p 28080 cable/config.ru
    ports:
      - '28080:28080'
    volumes:
      - '.:/app'
    env_file:
      - '.env'

volumes:
  redis:
  postgres:

Docker will use this file to setup the services like Postresql, you notice that it also has a env_file which uses the .env file we created previously.

Ok, now we will continue with the Dockerfile

FROM jruby:9.1.15-alpine

RUN apk update && apk add build-base nodejs postgresql-dev redis git

RUN mkdir /app
WORKDIR /app

COPY Gemfile Gemfile.lock ./
RUN bundle install --binstubs

COPY . .

CMD puma -C config/puma.rb

This is for me the most important part since this is what Docker needs to build the application.

We can't build this thing yet since the Rails application needs to be modified first...

Lets start with the config folder...

Modified the application.rb with the following code

  ...
    # Initialize configuration defaults for originally generated Rails version.
    # config.load_defaults 5.1

    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Set up logging to be the same in all environments but control the level
    # through an environment variable.
    config.log_level = ENV['LOG_LEVEL']

    # Log to STDOUT because Docker expects all processes to log here. You could
    # then redirect logs to a third party service on your own such as systemd,
    # or a third party host such as Loggly, etc..
    logger           = ActiveSupport::Logger.new(STDOUT)
    logger.formatter = config.log_formatter
    config.log_tags  = %i[subdomain uuid]
    config.logger    = ActiveSupport::TaggedLogging.new(logger)

    # Action mailer settings.
    config.action_mailer.delivery_method = :smtp
    config.action_mailer.smtp_settings = {
      address:              ENV['SMTP_ADDRESS'],
      port:                 ENV['SMTP_PORT'].to_i,
      domain:               ENV['SMTP_DOMAIN'],
      user_name:            ENV['SMTP_USERNAME'],
      password:             ENV['SMTP_PASSWORD'],
      authentication:       ENV['SMTP_AUTH'],
      enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
    }

    config.action_mailer.default_url_options = {
      host: ENV['ACTION_MAILER_HOST']
    }
    config.action_mailer.default_options = {
      from: ENV['ACTION_MAILER_DEFAULT_FROM']
    }

    # Set Redis as the back-end for the cache.
    config.cache_store = :redis_store, ENV['REDIS_CACHE_URL']

    # Set Sidekiq as the back-end for Active Job.
    config.active_job.queue_adapter = :sidekiq
    config.active_job.queue_name_prefix =
      "#{ENV['ACTIVE_JOB_QUEUE_PREFIX']}_#{Rails.env}"

    # Action Cable setting to de-couple it from the main Rails process.
    config.action_cable.url = ENV['ACTION_CABLE_FRONTEND_URL']

    # Action Cable setting to allow connections from these domains.
    # origins = ENV['ACTION_CABLE_ALLOWED_REQUEST_ORIGINS'].split(',')
    # origins.map! { |url| /#{url}/ }
    # config.action_cable.allowed_request_origins = origins
  ...

This will setup the logger, Action mailer, Active Job and Action Cable with the environment variables from .env thanks to the gem dotenv-rails.

Then lets modify the `cable.yml``

---

development: &default
  adapter: redis
  url: <%= ENV['ACTION_CABLE_BACKEND_URL'] %>

test:
  <<: *default

staging:
  <<: *default

production:
  <<: *default

This sets up the Action Cable configuration.

Then lets modify the database.yml

---

development:
  url: <%= ENV['DATABASE_URL'].gsub('?', '_development?') %>

test:
  url: <%= ENV['DATABASE_URL'].gsub('?', '_test?') %>

staging:
  url: <%= ENV['DATABASE_URL'].gsub('?', '_staging?') %>

production:
  url: <%= ENV['DATABASE_URL'].gsub('?', '_production?') %>

This also uses the .env to read the database url.

Then lets create the config for sidekig.yml.erb

---

:queues:
  - <%= ENV['ACTIVE_JOB_QUEUE_PREFIX'] %>_<%= ENV['RAILS_ENV'] %>_default
  - <%= ENV['ACTIVE_JOB_QUEUE_PREFIX'] %>_<%= ENV['RAILS_ENV'] %>_mailers

This setups the Sidekiq queues names.

You should be confortable with this kind of changes on the /config folder. So, lets continue doing more changes, in the initializers

Add a new file sidekiq.rb

Sidekiq.configure_server do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

This setups the Sidekiq redis URL, if you do not do it Sidekiq will not work.

Then lets create a new file timeout.rb

Rack::Timeout.timeout = ENV.fetch('REQUEST_TIMEOUT') { 5 }.to_i

I was struggling without this code... So do not forget to add it.

Then lets add a new file database_connection.rb

Rails.application.config.after_initialize do
  ActiveRecord::Base.connection_pool.disconnect!

  ActiveSupport.on_load(:active_record) do
    config = ActiveRecord::Base.configurations[Rails.env] ||
             Rails.application.config.database_configuration[Rails.env]
    config['pool'] = ENV['RAILS_MAX_THREADS'] || 16
    ActiveRecord::Base.establish_connection(config)
  end
end

This file is critical, since it is the configuration for the database connection, if you do not add it your Rails app will start but it wont connect to the database.

Finally, we are ready to build the app, run the command docker-compose up --build. Be patient since this command will download many libs necesary for the virtual machine work.

After that visit 192.168.99.100:3000 and you should see the Rails welcome page!

Challenges I faced

I was struggling with putting all the pieces together, it took me a while to investigate and debug a lot of problems I had by creating this project. For example: The Rails version 5.1.x does not work with the postgresql jdbc adapter version 51, so I used the Rails version 5.0.6 and the postgresql jdbc adapter version 50-stable. The other problem was on the Puma configuration, I was unable to set the workers configuration and I still do not not how to use it without breaking the app, so I commented it.

Key learnings

If you can create a JRuby on Rails application that runs on Docker this means that you are in a new level since your app can scale much better than the tipical ROR app + Heroku combo...

Tips and advice

Please check my fully functional JRuby on Rails application called Twitalytics here: https://github.com/victorhazbun/twitalytics, you can download it or forkit and play with it!

Final thoughts and next steps

In future lessons we will be working with the Twitalytics app, adding content to it and including powerfull features like: Deploying JRuby in the Enterprise, Managing a JRuby Application, Tuning a JRuby Application, Monitoring JRuby in Production, Using a Continuous Integration Server.

Hope you liked this post, and if so please support it by upvoting it!

Discover and read more posts from Victor H
get started
post commentsBe the first to share your opinion
Geraldo Andrade
6 years ago

After docker-compose up --build is done notice that database is not created yet.
In order to fix this once compose is done enter the container usingdocker-compose exec website ash and then just run RAILS_ENV=development rake db:create. It should fix development environment.

Show more replies