Give Codeship’s CI/CD Platform a Try

Want to learn more?

As we prepare our new Docker-based infrastructure for running your tests, we’d like to show you how you can use the same environment for development as well. Feel free to follow along as I demonstrate how to move a simple Rails applications into Docker during development.

Suppose we have a (very) simple new Rails application:

gem install rails bundler
rails new demo
cd demo
bundle install

Without any further configuration, this provides us with a Rails application, using SQLite as a database. And if we want to, we can run our tests and start the development server to take a look in our browser.

bundle exec rake test
bundle exec rails server

We can now access the default start page at localhost:3000. It’s not a very useful app, but it’ll do for our purpose.

Now, let’s move this app to use in a Docker-based environment.

Step 1: Installing Docker

If you already have Docker up and running, you can skip this step and move on to Step 2, Dockerizing right away. If not, let’s get Docker running on your machine.

At Codeship, we recommend using Docker Machine. It’s a young project and still in beta, but we’ve had great success using it internally.

See their installation instructions for how to get it running on your computer. Those instructions will include the necessary commands to get Docker itself running as well. Once you have Docker Machine installed, create a new environment (e.g., based on Virtualbox) and configure your local host to use that environment for Docker.

Step 2: Dockerizing a Rails Application

Now that we have Docker installed and running, it is time to get our application running on it. Docker applications are configured via a Dockerfile, which defines how the container is built.

The easiest Dockerfile includes a single line, the base image to use. The following one would, for example, provide an Ubuntu Trusty based system:

FROM ubuntu:14.04

Many images are readily available, and you can search for a suitable base image at the Docker Hub. We’ll use the ruby:2.2 base image.

FROM ruby:2.2
MAINTAINER marko@codeship.com
# Install apt based dependencies required to run Rails as
# well as RubyGems. As the Ruby image itself is based on a
# Debian image, we use apt-get to install those.
RUN apt-get update && apt-get install -y \
build-essential \
nodejs
# Configure the main working directory. This is the base
# directory used in any further RUN, COPY, and ENTRYPOINT
# commands.
RUN mkdir -p /app
WORKDIR /app
# Copy the Gemfile as well as the Gemfile.lock and install
# the RubyGems. This is a separate step so the dependencies
# will be cached unless changes to one of those two files
# are made.
COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install --jobs 20 --retry 5
# Copy the main application.
COPY . ./
# Expose port 3000 to the Docker host, so we can access it
# from the outside.
EXPOSE 3000
# The main command to run when the container starts. Also
# tell the Rails dev server to bind to all interfaces by
# default.
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

After adding the above file as Dockerfile to your repository, we can now build the container and start running commands with it. We specify a tag via the -t option, so we can reference the container later on.

docker run runs tasks in a Docker container. This is most commonly used for one-off tasks but is also very helpful in development.

The -P option causes all ports defined in the Dockerfile to be exposed to unprivileged ports on the host and thus be accessible from the outside.

If we don’t specify a command to run on the command line, the command defined by the CMD setting will be run instead.

We now have our Rails application running inside a Docker container, but how do we actually access it from our computer? We will use docker ps, a handy tool to list running Docker processes as well as additional information about them.

We can see the container ID, the image it is based on, which command it is running, and the mapping of any exposed ports. With this information at hand, we can now open the app in our browser http://localhost:32769.

Note: If you don’t have Docker running on your local machine, you need to replace localhost in the above URL with the IP address of the machine Docker is running on. If you’re using Docker Machine, you can run docker-machine ip “${DOCKER_MACHINE_NAME}” to find out the IP.

The application is up and running and accessible from our development machine. But, each time we make a change, we need to build a new container. That’s not very helpful. Let’s improve our setup.

Step 3: Docker Volumes

Docker supports what it calls volumes. These are mount points which let you access data from either the native host or another container. In our case, we can mount our application folder into the container and don’t need to build a new image for each change.

Simply specify the local folder as well as where to mount it in the Docker container when calling docker run, and you’re good to go!

docker run -itP -v $(pwd):/app demo

Step 4: Improvements

Dockerfile Best Practices lists some ways to improve performance and create easy to use Dockerfiles. One of those tips is using a .dockerignore file.

.dockerignore

Similar to a .gitignore file, .dockerignore lets us specify which files are excluded and not transferred to the container during the build. This is a great way to speed up the build times, by excluding files not needed in the container (e.g., the .git subdirectory). Let’s add the following .dockerignore file to our project

Entrypoint

Because most of the commands we run on the Rails container will be prepended by bundle exec, we can define an [ENTRYPOINT] for all our commands. Simply change the Dockerfile like this:

# Configure an entry point, so we don't need to specify
# "bundle exec" for each of our commands.
ENTRYPOINT ["bundle", "exec"]
# The main command to run when the container starts. Also
# tell the Rails dev server to bind to all interfaces by
# default.
CMD ["rails", "server", "-b", "0.0.0.0"]

You can now run commands without specifying bundle exec on the console. If you need to, you can override the entrypoint as well.

Locales

If you’re not happy with the default locale in your Docker container, you can switch to another one quite easily. Install the required package, regenerate the locales, and configure the environment variables.

With the configuration above, running your development environment is as simple as running two commands:

docker-compose build
docker-compose up

Even for a single container environment this has some (smaller) improvements over using docker directly. We can specify the VOLUME definition directly in the configuration file; we don’t need to specify it on the command line. We can also define the port on the Docker host our application will be available at and don’t need to look it up.

Adding PostgreSQL

We could now create a new Dockerfile for running PostgreSQL, but luckily we don’t need to. There is a readily available PostgreSQL Docker image available on the Docker Hub, so let’s just use that instead.

We defined a new container called postgres, based on the PostgreSQL 9.4 image (there are images for previous versions available as well), configured the port on the new image, and told our app container to define a link to the database.

But how do we access the database from within our Rails application? Fortunately for us, Docker Compose exposes environment variables for linked containers, so let’s take a look at those.

Update, Compose now recommends to use the hostnames instead of environment variables to access linked services. The database.yml mentioned above should now look like the following snippet, further changes are not required.

Step 5.5: Extending Your Rails Development Environment

With the steps shown above, it’s easy to extend the rails development environment. Need a Redis server? Look for a suitable image on the Docker Hub, extend the docker-compose.yaml configuration file with a few lines, and restart your environment. The same goes for memcached or other services you require.

Want to test against a different Ruby version? Modify the Dockerfile, spin up the environment and run your tests.

Conclusion

Switching your development environment to Docker does take some amount of work, but the benefits are well worth it. You get an environment that’s easy to share with fellow team members, you can model it to closely resemble your production environment, and extend it in a simple way. With Codeship’s upcoming Docker infrastructure, you’ll be able to use the same environment to run your tests on Codeship and then push the built and tested containers to your production servers and deploy them. Easy, fast, efficient, and with less room for errors.

The best part is that it’s easy to clean up once you’re done with a project. You don’t pollute your computer with all those different libraries you only need for that single project!

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles. Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.

We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.