Dockerize a Rails App with MySQL and Sidekiq

(S//Bot) #1

Dockerising an application is creating an isolated environment with containers for different moving parts in an application which makes the development and deployment easier and you don’t have to repeat yourself everytime when you need to deploy it locally for development or in production. It’s one command away and it’s that’s easy when you dockerize it. Dockerising is fun and tricky in the beginning when you define volumes for containers and working out an architecture that’ll suit your application. Okay, let’s just stop the talking and dive right into it. I’ll first start with the introduction to Docker and what you’ll need to dockerize an application. I take up a Rails App for this blog post but you can dockerize literally anything.

The prerequisites are to install Docker and make sure it’s running. Install Ruby & Rails. Don’t worry we’re not gonna deal with ruby.

“Hey, Docker. Who are you?”

I’m not gonna describe it to you. I’ll ask Docker to do that part. It’ll be so good if he does that by himself. So here he is.

Subscribe to Sudo vs Root

Our newsletter rolls out every 15 days. No fluff. Pure content.

“Hey! I’m a tool designed to make it easier to create, deploy, and run applications by using containers. A container is a part of me. A container is a standard unit of software that packages up the code and all its dependencies so the application runs quickly and reliably from one computing environment to another. A container image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries, and settings. By doing so, thanks to my container, you can rest assured that the application will run on any other Linux machine regardless of any customized settings that machine might have that could differ from the machine used for writing and testing the code. The important thing to note is that I’m Open source and you can contribute to making me better. See how awesome I’m! :D”

So, how was his introduction? Okay, Let’s move to the other aspects in detail. Let’s talk about Docker images and Containers.

Volumes, Docker Images & Containers

Volumes are the preferred mechanism for persisting data generated by and used by Docker containers. While bind mounts are dependent on the directory structure of the host machine, volumes are completely managed by Docker.

Docker containers are based on Docker images. A Docker image is a binary that includes all of the requirements for running a single Docker container, as well as metadata describing its needs and capabilities. You can think of it as a packaging technology. Docker containers only have access to resources defined in the image, unless you give the container additional access when creating it.

Dockerfile and docker-compose.yml

A Dockerfile is used to build an image. A Compose file is used to deploy a container from an image.

If you’re using a public image, such as Nginx or MySQL, then there’s no need for a Dockerfile since you don’t need to build it yourself - it’s already built and accessible via Docker Hub, so your Compose file can just pull it from there.

You usually don’t need a Dockerfile unless you’re creating a custom container image from scratch, or customizing a public image for some reason.

So, we’ll need Dockerfile to build our custom images for the web-app, sidekiq for our ruby application. Since we have pre-built images for MySQL, Ruby, and Redis, we’ll just pull the image from Docker Hub. We’ll talk in detail about everything.

We’re half-way through and let’s get started in dockerizing a rails application.

via GIPHY

rails new app_name

As we discussed earlier, we’ll work on a rails application. Let’s create a new application with the command rails new rails-mysql-docker where rails-mysql-docker is the name of the project. This will create a scaffold and once it’s complete, open the folder in a code editor. Now, let’s get the basics done. We’ll install the MySQL gem as we’ll be using MySQL as the database and sidekiq for background processing. At the later point of the blog, we’ll talk about sidekiq. Now, add the required gems to the Gemfile and run bundle install in the command line.

Now, I assume that you’ve installed Docker on your computer and made sure that it’s up and running. We now created a rails app and installed mysql gem in it.

“It’s Docker Time!!”

via GIPHY

We’ll now be writing the Dockerfile and the docker-compose.yml file. As I said previously, A Dockerfile is used to build an image. A Compose file is used to deploy a container from an image. We’ll first write the Dockerfile. The Dockerfile contains commands that need to be executed while building the image. That may include system libraries and other stuff. Create a file named Dockerfile at the root of the project and add the following to it.

FROM ruby:2.5-alpine
RUN apk update && apk upgrade && apk add ruby ruby-json ruby-io-console ruby-bundler ruby-irb ruby-bigdecimal tzdata postgresql-dev && apk add nodejs && apk add curl-dev ruby-dev build-base libffi-dev && apk add build-base libxslt-dev libxml2-dev ruby-rdoc mysql-dev sqlite-dev
RUN mkdir /app
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN gem install ovirt-engine-sdk -v '4.3.0' --source 'https://rubygems.org/'
RUN bundle install --binstubs
COPY . .
EXPOSE 3000
ENTRYPOINT ["sh", "./config/docker/startup.sh"]

The above Dockerfile contains some commands that are run to build a Docker image. We’ll now break out the above docker file.

We’re asking it to use the base image ruby for the 2.5-alpine distribution.

RUN apk update && apk upgrade && \
apk add ruby ruby-json ruby-io-console ruby-bundler ruby-irb ruby-bigdecimal tzdata && \
apk add nodejs && \
apk add curl-dev ruby-dev build-base libffi-dev && \
apk add build-base libxslt-dev libxml2-dev ruby-rdoc mysql-dev sqlite-dev

This will install the required system libraries. The above has some extra libraries which are not quite needed for a small rails application.

RUN mkdir /app
WORKDIR /app

We now run a mkdir command which is used to create a new directory. The WORKDIR line specifies a new default directory within the image’s file system which is the app directory.

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

Now, copy the Gemfile, lock file and the current directory and run bundle install. When you use COPY it will copy the files from the local source, in this case . meaning the files in the current directory, to the location defined by WORKDIR. In the above example, the second. refers to the current directory in the working directory within the image.

EXPOSE 3000
ENTRYPOINT ["sh", "./config/docker/startup.sh"]

This will export the port 3000 and run a startup.sh shell script. We use some shell scripts to build the application. We’ll talk about it more in some time.

An ENTRYPOINT allows you to configure a container that will run as an executable. In our case, it will run startup.sh that will build our app.

We also don’t want unwanted files inside a container. Take git tracking for example. We don’t need it to run our app. Thus, it is not needed. So, we create a .dockerignore file that contains the following,

.dockerignore
.git
logs/
tmp/

docker-compose.yml

The Compose file is a YAML file which defines services, networks, and volumes. It usually helps us defining the containers and, volumes with ENV variables. We’ll break out every container.

version: "3.7"
services:
 db:
   image: "mysql:5.7"
   environment:
     MYSQL_ROOT_PASSWORD: root
     MYSQL_USERNAME: root
     MYSQL_PASSWORD: root
   ports:
      - "3307:3306"
 redis:
   image: "redis:4.0-alpine"
   command: redis-server
   volumes:
      - "redis:/data"
 website:
   depends_on:
      - "db"
      - "redis"
   build: .
   ports:
      - "3000:3000"
   environment:
     DB_USERNAME: root
     DB_PASSWORD: root
     DB_DATABASE: sample
     DB_PORT: 3306
     DB_HOST: db
     RAILS_ENV: production
     RAILS_MAX_THREADS: 5
   volumes:
      - ".:/app"
      - "./config/docker/database.yml:/app/config/database.yml"
 sidekiq:
   depends_on:
      - "db"
      - "redis"
   build: .
   command: sidekiq -C config/sidekiq.yml
   volumes:
      - ".:/app"
   environment:
     REDIS_URL: redis://redis:6379/0
volumes:
 redis:
 db:

“Breaking it up is better than breakup”. Ok, I get it. It’s so dumb. Now we’ll break out our docker-compose.yml.

In the docker-compose.yml file, the version depends on the docker release. There is a table in the docker documentation that maps different version with their respective docker releases. The services are what that makes up your application. It consists of all the services that will be built as different containers that act as the organs of your application. We’ll have about 4 services namely - db, redis, website, sidekiq.

db:
  image: "mysql:5.7"
  environment:
    MYSQL_ROOT_PASSWORD: root
    MYSQL_USERNAME: root
    MYSQL_PASSWORD: root
  ports:
    - "3307:3306"

This container is for installing our MySQL and running it as a container. We pull an already built image mysql:5.7 from docker hub. We also pass the required environment variables. Ports mentioned in docker-compose.yml will be shared among different services started by the docker-compose. Ports will be exposed to the host machine to a random port or a given port.

redis:
  image: "redis:4.0-alpine"
  command: redis-server
  volumes:
    - "redis:/data"

Same as MySql, we pull a redis image from docker hub. Compose includes the ability to attach volumes to any service that has persistent storage requirements. We’ll have volumes to contain our persistent data like the above. The command will run after the container is built. Docker will create the volume for you in the /var/lib/docker/volumes folder. This volume persists as long as you are not typing docker-compose down -v.

website:
  depends_on:
    - "db"
    - "redis"
  build: .
  ports:
    - "3000:3000"
  environment:
    DB_USERNAME: root
    DB_PASSWORD: root
    DB_DATABASE: sample
    DB_PORT: 3306
    DB_HOST: db
    RAILS_ENV: production
    RAILS_MAX_THREADS: 5
  volumes:
    - ".:/app"
    - "./config/docker/database.yml:/app/config/database.yml"

The website service is where our rails application resides. docker-compose up will start services in dependency order as defined with depends_on. In the following example, db and redis will be started before the website. The build key tells to build with the docker-compose.yml present in the current directory.

sidekiq:
  depends_on:
    - "db"
    - "redis"
  build: .
  command: sidekiq -C config/sidekiq.yml
  volumes:
    - ".:/app"
  environment:
    REDIS_URL: redis://redis:6379/0

Sidekiq is a simple, efficient background processing for Ruby. Sidekiq uses threads to handle many jobs in the same process simultaneously. It does not require Rails but will integrate tightly with Rails to make background processing dead simple. A rails app will definitely need redis and sidekiq for background processing.

Docker Utility Folder

We’ll have a docker utility folder inside config that’ll contain a database.yml which will be later mounted into the config folder that’ll be used by the application. We’ll have four shell scripts.

startup.sh

If you remember the Dockerfile (If not, you can scroll back though :p), we would’ve referred startup.sh in our ENTRYPOINT. Yes, you guessed it right. We’re nearly at the end of the tutorial.

#! /bin/sh
# Wait for DB services
sh ./config/docker/wait-for-services.sh
# Prepare DB (Migrate - If not? Create db & Migrate)
sh ./config/docker/prepare-db.sh
# Pre-comple app assets
sh ./config/docker/asset-pre-compile.sh
# Start Application
bundle exec puma -C config/puma.rb

This script will run three more scripts and starts our application. We’ll discuss that below.

wait-for-services.sh

This script polls and waits for the MySQL to be up and running with the help of the host and the port it is running.

#! /bin/sh
# Wait for MySQL
until nc -z -v -w30 $DB_HOST $DB_PORT; do
 echo 'Waiting for MySQL...'
 sleep 1
done
echo "MySQL is up and running!"

prepare-db.sh

This script handles the database migrations for the application. It’ll create the database and migrate if the migrations failed in the first try.

#! /bin/sh
# If the database exists, migrate. Otherwise setup (create and migrate)
bundle exec rake db:migrate 2>/dev/null || bundle exec rake db:create db:migrate
echo "Done!"

asset-pre-compile.sh

We use rake assets:precompile to precompile our assets before pushing code to production. This command precompiles assets and places them under the public/assets directory in our Rails application.

#! /bin/sh
# Precompile assets for production
bundle exec rake assets:precompile
echo "Assets Pre-compiled!"

database.yml

As said before we’ll have the database configurations in our docker folder which will then be mounted into the config folder that’ll be then used by the rails application.

default: &default
 adapter: mysql2
 encoding: utf8mb4
 collation: utf8mb4_bin
 reconnect: false
 pool: 50
 username: <%= ENV['DB_USERNAME'] %>
 password: <%= ENV['DB_PASSWORD'] %>
 port: <%= ENV['DB_PORT'] %>
 host: <%= ENV['DB_HOST'] %>
 socket: /var/run/mysqld/mysqlx.sock
development:
 <<: *default
 database: <%= ENV['DB_DATABASE'] %>_development
production:
 <<: *default
 database: <%= ENV['DB_DATABASE'] %>

#ItsDone

Now, all we need to do is run docker-compose up -d. The -d is Detached mode which will run the containers in the background. The app will be built with all the containers and you can view the containers with docker ps -a. You can also find the GitHub Repo or follow me on Twitter for some retweet spam. In my next article, we’ll talk about CI/CD with Jenkins. Until next time, have this Gif for free.

via GIPHY


This is a companion discussion topic for the original entry at https://www.skcript.com/svr/dockerize-a-rails-app-with-mysql-and-sidekiq/