This post originally appeared on the Scout blog.
In a recent post, we talked about the 8 things that you know about Docker containers, and what you should know about them. Hopefully we cleared up any confusion you might have had about the Docker ecosystem. Perhaps with all that talk, it got you thinking about trying it out on one of your own applications? Well in this post we’d like to show you how easy it is to take your existing Ruby on Rails applications and run them inside a container. So, let’s assume you have an existing Rails project with a PostgreSQL database, and let’s walk you through the steps it would take to run this in a container instead. It’s a lot easier than you probably think!
The first thing that we need to do to get our application to run in a container is to define our custom image that we will run as a container, and we can do this in a Dockerfile. This Dockerfile is essentially a set of instructions for Docker to use when it builds our container image. The idea is that this file is all that is required to produce an identical container on any system, and so we can add this file to source control so that everybody in our team can utilize it. Let’s create the following file called "Dockerfile" and place it inside our project’s root directory:
# Start from the official ruby image, then update and install JS & DB FROM ruby:2.6.2 RUN apt-get update -qq && apt-get install -y nodejs postgresql-client # Create a directory for the application and use it RUN mkdir /myapp WORKDIR /myapp # Gemfile and lock file need to be present, they'll be overwritten immediately COPY Gemfile /myapp/Gemfile COPY Gemfile.lock /myapp/Gemfile.lock # Install gem dependencies RUN bundle install COPY . /myapp # This script runs every time the container is created, necessary for rails COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Start rails CMD ["rails", "server", "-b", "0.0.0.0"]
This is the entrypoint.sh file that we need to create and add to our project:
#!/bin/bash # Rails-specific issue, deletes a pre-existing server, if it exists set -e rm -f /myapp/tmp/pids/server.pid exec "$@"
So using this Dockerfile and the entrypoint.sh script, we can build our image with a single command. But you might have noticed that we haven’t yet specified our PostgreSQL database details yet. Database engines usually run in their own containers, separate from your web application. The good news is that we don’t have to define a custom image with a Dockerfile for this database container, we can just use a standard PostgreSQL image from Docker Hub and use it as it is. But it does still mean that we will have two separate containers that need to communicate with each other. How do we do that? That’s where Docker Compose comes in.
Docker Compose is a tool that allows us to connect multiple containers together into a multi-container environment that we can think of as a service. For example, in our situation we have a database engine container and a rails environment container. So we could use Docker Compose to combine these into a multi-container environment which we can conceptually view as our complete application. To use Docker Compose, we need to create a docker-compose.yml file, in addition to the Dockerfile that we already have and place this in your project’s directory. This file will tie together the rails container we previously defined in the Dockerfile (let’s call that "web"), and another container for the database which we will call "db":
version: '3' services: db: image: postgres volumes: - ./tmp/db:/var/lib/postgresql/data web: build: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp ports: - "3000:3000" depends_on: - db
As you can see, for the db part, we just specify the ‘postgres’ image from Docker hub and then mount the location of our database into the container. For the web part however, we build the image defined in our Dockerfile, run some commands and mount our application code into the container. Note that if you are using a system that enforces SELinux (such as Red Hat, Fedora or CentOS), then you will need to append a special :z flag to the end of your volume paths.
So now that we have a Dockerfile, docker-compose.yml and entrypoint.sh script in our project’s directory, there are just a few more steps we need to do before we build our image and run the application as a container.
First of all, it is a good idea to clear out the contents of our Gemfile.lock file before we proceed:
$ rm Gemfile.lock $ touch Gemfile.lock
Next, we need to update the database settings, as the credentials for the PostgreSQL image differ from the credentials you would use on your local install. The main difference is that here we specify our ‘db’ container as the host. You can make these changes to the relevant part of your config/database.rb file:
default: &default adapter: postgresql encoding: unicode host: db username: postgres password:
The next step is to build our custom image that we defined in the Dockerfile, we can do that like this:
$ docker-compose build
Now we have an image for our web application, we need to prepare our database (inside the container).
$ docker-compose run web rake db:create $ docker-compose run web rake db:migrate
And that’s it! We’re done! All you need to do now to run you entire application (this time, and in future) is this one command:
$ docker-compose up
When you are finished, to shut down correctly and remove your containers, you run:
$ docker-compose down