Nowadays, Docker integration is a part of almost every software application lifecycle. It invaded the world of modern development, along with Microservices architecture patterns, a few years ago and enables resolution of deployment and scalability problems in large, sophisticated applications. According to the latest reports containerisation is rapidly gaining popularity in companies of all sizes and types.
In this article I am going to show how a Rails 5 application can be set up with Docker containers running on a Passenger server. We will start with the creation of a blank Rails app but the following steps are applicable for existing Rails apps as well.
$ rails new simple-app
Once the generation and gem installation processes have finished, run the app:
$ cd simple-app
$ rails s
and then confirm that everything works by accessing http://localhost:3000 in your web browser. If you see the following image then everything is OK so far:
Docker
So what is Docker?
If you do not have Docker-engine installed yet, you can follow the official Docker installation guide.
Docker containers are run from images. Basically an image is an isolated operating system with a pre-installed set of libraries/frameworks defined in a Dockerfile or inherited from another image. Images are built with a special command: docker build.
Thus our goal is to build our own image which can then be run as a staging or production server. As you may have already guessed, we’re going to start by creating a Dockerfile just inside the app directory with the following contents:
FROM phusion/passenger-full:latest
EXPOSE 80
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
RUN bash -lc 'rvm --default use ruby-2.3'
# nginx confs
RUN rm -f /etc/service/nginx/down
# Remove the default site
RUN rm /etc/nginx/sites-enabled/default
ADD webapp.conf /etc/nginx/sites-enabled/webapp.conf
ADD rails-env.conf /etc/nginx/main.d/rails-env.conf
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install
ADD . /app
RUN chown -R app:app /app
A Dockerfile is a set of instructions on how to build an image. Here is an explanation of each line:
FROM phusion/passenger-full:latest
The FROM command takes the image from the Docker Hub on which the new image will be based, with a tag number after the colon. In our case we will take the passenger-full image with the latest tag. We will use this feature to manage deployed versions of our app.
EXPOSE 80
Opens port 80 of the container which will be launched from the new image.
ENV APP_HOME /app
Defines $APP_HOME variable to ‘/app’. We choose this path because the phusion/passenger image has an app user with UID 9999 and home directory /home/app. Therefore our application should be placed inside /home/app.
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
Creates and sets /app directory as the working directory. This means that from this directory all RUN, ADD, etc commands will be executed from this directory.
RUN bash -lc 'rvm --default use ruby-2.3'
Sets ruby version to 2.3 (this is available because the passenger-full image has several pre-installed versions of ruby with RVM).
RUN rm -f /etc/service/nginx/down
Enables Nginx and Passenger.
RUN rm /etc/nginx/sites-enabled/default
This is needed to remove the Nginx site as default (“Welcome to Nginx” page)
ADD webapp.conf /etc/nginx/sites-enabled/webapp.conf
ADD rails-env.conf /etc/nginx/main.d/rails-env.conf
These two files in our configuration should contain passenger settings and rails environment variables which we will be able to use while starting a container. The general idea is to have an environment independent container which can be run with any settings. Here is how the settings may look:
/webapp.conf
server{ listen 80; server_name _; root /app/public; passenger_enabled on; passenger_user app; passenger_min_instances 2; passenger_max_request_queue_size 500; } passenger_max_pool_size 4;
/rails-env.conf
env RAILS_ENV; env SECRET_KEY_BASE;
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install
Obviously we will need to run bundle install for each deployment, but there is a trick- simply add Gemfile and Gemfile.lock separately. This isn’t necessary but it allows you to make Docker “watch” these files and cache inside intermediate images while running the first build. All subsequent builds will therefore be much faster unless new gems are added.
ADD . /app
RUN chown -R app:app /app
Lastly, copy the application directory into the /app directory within the image and give the app user ownership of the directory.
And we’re done! Now let’s build an image and run our first container:
$ docker build -t simple-app
Once the image is ready it is possible to confirm it by running:
$ docker images
And finally, we just need to run the container:
$ docker run -d -m 2G \ -e PASSENGER_APP_ENV=development \ -e RAILS_ENV=development \ -e SECRET_KEY_BASE='0097ee3b2496e3b6cdfddbc0f96aa78b4d8ea153047ef1b00eb31e51fc15a5e3ad0fdec380fb880ffbfdaff95bbbc12c267cd34dae1f760bc8e2c0e7b478b5c2' \ --restart="always" \ --name simple-app \ -p 3000:80 simple-app
Here the -d option stands for daemon, -m – is the amount of memory which will be dedicated to the container. and the -e options are the environment variables which we declared in rails-env.conf. PASSENGER_APP_ENV is a variable defined by default in passenger image.
To confirm that our container is up and running execute
$ docker ps
This will list all running containers and we should be able to see simple-app. Now we can access the Passenger server of our app in the same way http://localhost:3000. As we published the container’s port 80 to the host’s port 3000 with the -p option, you should see the same page as you do while running the app locally.
And that’s it :). The process above looks pretty much the same for any staging or production environment setup. Once a container is built it can be run anywhere.
Good to know
Here are a few tips and links which may be useful:
1. If you want to run a container from another image, firstly stop and remove the current one like this:
$ docker rm simple-app
$ docker stop simple-app
2. If you have any problems with running a container, you can check its logs with:
$ docker logs simple-app
3. It is also possible to access the terminal of a container by:
docker exec -it simple-app /bin/bash
And then inside check Nginx logs in /var/log/nginx/error.log or access.log
4. If you are going to run a container inside a private network with access to other services, it is possible by giving it access to servers hosts file by adding a volume:
-v /etc/resolv.conf:/etc/resolv.conf
5. It is possible to tag each container which can be used as a version in each deployment:
docker build -t simple-app:1
docker run (...) simple-app:1
Additional information about the phusion passenger image can be found on the official github page. For example the passenger-full image has already included many ruby versions along with redis, node, etc.
Conclusion
So, as we can see, Docker-ising a Rails app generally isn’t such a long and complicated process. What are main benefits of using Docker though?
- It solves scalability problems, running another container is a matter of one command.
- It keeps deployments easy. The deployment process is turned into just building a new image.
- It is fast to revert if needed. Simply run the image with a previous tag/version.
- It fits perfectly into the micro services approach.
- It is environment independent and can be used for the fast setup of any application locally for development purposes.
In this article, we haven’t covered many cases with e.g. connecting an app to external services or running a service inside a container, but I hope that the information described above can become a solid background in helping you to understand how Rails can be integrated with Docker to become a part of any micro services structure.
2 thoughts on “How Docker Can Simplify Your Life”
Thank you, the tutorial is really useful!
You have to put a period in the end of the command “docker build -t simple-app” like that: “docker build -t simple-app .”