Deploying web apps with Mina and Docker

Alvaro Fernando Lara
Alliants
Published in
4 min readMay 6, 2015

--

by Alvaro Lara

TL;DR: Docker gives us the mechanism to pack our application and all its dependencies into a lightweight, runnable container. Mina is a tool for setting up the servers and deploying applications. Both do great on their own, but combined do even better. Check out my script in this gist.

Running an app in development is just a half the success. The goal is to deploy it to a staging or production server which often have a different configuration from the development machine. Consider the following scenario:

  • Rails 4.2 app working as an API
  • ActiveJob workers for some background processing tasks
  • MySQL as the database
  • RabbitMQ as the queueing mechanism

The list of things that I need to do to get the app working on every new server without mina and docker:

  • Install main dependencies: mysql, git, build-essentials, rabbitmq, nginx, ruby-install, ruby 2.2, rabbitmq dependencies, mysql related libs for ruby
  • Copy the version of the application into the server on every deploy
  • Create appropriate file structure for subsequent deploys (once)
  • Setup nginx and expose the app (once)
  • Write appropriate application specific configuration (once)
  • Start/Stop the application on every deploy

The first natural step was to get Mina to deploy the rails app, to solve the problem of moving code, structuring files, nginx configuration and starting/stopping the application.

This reduced the list of things to be done manually to:

  • Install dependencies
  • Setup nginx and expose the app (once)
  • Write appropriate application specific configuration (once)

Now each deployment looks like:

$ mina deploy; mina unicorn:restart

Although this is a pretty good approach, due to the development environment not always being similar to production, being able to replicate the environment and test new features locally can be error prone. Docker is lightweight and can run on both in Linux and Mac, with help from boot2docker, concentrating all of our app dependencies. Once we got the configuration right we can focus on the work at hand rather than setting up the environment.

This is the Dockerfile I’m using:

Once I had an appropriate Dockerfile for my app, the list of things to be done manually looked like:

  • Install mysql, git, rabbitmq, nginx and docker (once)
  • Setup nginx expose the app (once)
  • Write appropriate application specific configuration (once)

With a bit of help from mina I got it to build the images, stop/start containers and link them to the appropriate configuration.

Finally now my deploy looks like:

$ mina deploy

Problems and solutions I found along the way

Working with docker on the server

To make it easier for developers to work with docker on the server, I introduced the following tasks in mina:

  1. docker:build It builds a new image based on the current deploy.
  2. docker:run It starts a new container for the specified image.
  3. docker:stop It stops a specific container
  4. docker:debug It creates a new container, and starts a terminal interactive mode with bash. Particularly handy when trying to identify issues with configuration and such.

Available options:

  • IMAGE_NAME: name of the docker image. This can either be used at the time of building a new image or when starting/debugging an existing one.
  • CONTAINER_NAME: Name to be assigned to the new container. Is best to give names and reserve them so we know exactly who is doing what. This can be used when starting or stopping containers.
  • DOCKERFILE: name of the Dockerfile to be used when building an image.

All this have default values:

set :image_name, ENV.fetch(“IMAGE_NAME”, “default_image_name”)set :dockerfile, ENV.fetch(“DOCKERFILE”, “Dockerfile”)set :container_name, ENV.fetch(“CONTAINER_NAME”, “service”)

An example of usage is:

mina docker:build IMAGE_NAME=my_image:v1 DOCKERFILE=Dockerfile2

Each new deploy should mean a new version of the docker image

As each PR in git should mean a new feature (or bugfix), I like my docker images to respect that same idea. So for each deploy I would like to create new versions of the same image. For that, I introduced the IMAGE_NAME parameter. Allowing all methods to specify which is the version of the image they want to run. It’s handy specially when having multiple dockerfiles, and for rolling back purposes.

The deploy command is:

mina deploy IMAGE_NAME=my_app:v1

Having more than one Dockerfile

The application not only required the server to be running but also workers to process the rabbimq messages. Because each container will perform only one task, the need to have a second Dockerfile with a different entrypoint/cmd emerged.

The solution we came up was to create a new Dockerfile with different a entrypoint living in the same repository. So now at the time of building, we needed to specify which Dockerfile we need to use when building and running.

The deployment now is:

mina deploy IMAGE_NAME=worker:v1 Dockerfile=DockerfileWorker CONTAINER_NAME=rabbit_worker

Stopping nonexisting docker containers

When automating the process of building new images, stopping the old containers and starting the new ones, I found that in situations where the container was not working, with the first deployment or when the container crashed, docker exits with a non zero return value since the container to be stopped is not running.

As you can imagine, this prevented my in progress deployment to finish successfully. So to work around this issue, I modified the `docker:stop` task to first check if the container was running before trying to stop it:

desc “Stop docker container”task stop: :environment do  queue “if [ ! -z \”$(sudo docker ps | grep ‘#{container_name}’)\” ]; then sudo docker stop #{container_name}; sudo docker rm -f #{container_name}; fi”end

--

--

Alvaro Fernando Lara
Alliants

Software person, loves running and enjoying the process rather than the outcome. Working at Onfido to help bring identities online.