Give Codeship’s CI/CD Platform a Try

Want to learn more?

In a previous article by Marko Locher, we learned how to run a Rails development environment in Docker. Marko also wrote about how to test a Rails app with Docker. So assuming we have our dev environment set up, our app is tested (and the tests are passing), we’re ready to think about deploying our application to production. If you haven’t yet, I recommend reading those two articles first to get a good overview of how Docker works.

In this article, we’ll talk about how to deploy our Docker container quickly and simply to Heroku. Once we’re done with that, we’ll investigate how to build a production Docker image using a minimal base which doesn’t include development dependencies. Then we’ll push our image to the Docker Hub and deploy it to a server.

Simple Rails Docker on Heroku

Most Rails developers are familiar with Heroku and have probably at one time or another deployed an app to it, even if it was just a personal blog or something you were playing around with. I’ve used Heroku both for personal projects and also for larger client projects with lots of success. Heroku isn’t just for Rails apps… you can deploy PHP apps, NodeJS apps, Elixir apps, and as of earlier this year you can even deploy Docker containers.

We’ll be using the Puma web server for this Rails application, and we need to set up a config file located in config/puma.rb. Please make sure that you have puma in your Gemfile as well!

port ENV['PORT'] || 3000
environment ENV['RACK_ENV'] || 'development'

Up and running locally

To run this app locally in the Docker container you can use the following command, which at this point is straight-up Docker and Docker Compose (rather than something specific to Heroku):

docker-compose up web

This command will open the app in the browser with the correct IP address:

open "http://$(docker-machine ip default):8080"

If all worked well you should be able to see your app running within the Heroku/Ruby Docker container.

Deploying to Heroku

To deploy to Heroku we’ll first need to make sure we have created an app on Heroku. Take note of the name they gave to your app in the output.

heroku create

After that we can deploy it using the following command. Keep in mind that you should use your app name on Heroku, not mine:

heroku docker:release --app warm-fortress-1700

Once it is done deploying (which will take a few minutes the first time as it uploads the somewhat large image slug to Heroku), you can open it in the browser using the command heroku open.

Building a Minimal Production Image

When building for a production release, you’ll want to keep your Docker images as lightweight as possible to avoid using up server resources. One of the ways you can do this is by using a trimmed-down Linux distro such as Alpine. CenturyLink has a great Docker image available that we can use as our base image.

I have created a Dockerfile.prod file as a way to add a couple extra commands that aren’t needed when working in a development environment.

FROM centurylink/alpine-rails
# 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 --without development test
# Set Rails to run in production
ENV RAILS_ENV production
ENV RACK_ENV production
# Copy the main application.
COPY . ./
# Precompile Rails assets
RUN bundle exec rake assets:precompile
# Start puma
CMD bundle exec puma -C config/puma.rb

In this file I’ve run bundle install with the --without development test flag to avoid Gems we don’t need in our production environment. I’ve set ENV variables called RAILS_ENV and RACK_ENV to production. And lastly I’ve precompiled the assets so that the image will already have all precompiled assets and we won’t need to do that again after deploy. It’s all about making sure our production image is as efficient and small as possible.

Let’s build our production image:

docker build -t leighhalliday/rails-alpine -f ./Dockerfile.prod .

Notice we’ve included the -f directive with a filename to point to our Dockerfile.prod file instead of the usual Dockerfile.

Pushing Our Image to Docker Hub

We’ve built our image locally, but it’s time to push it to Docker Hub. Docker Hub functions very much like GitHub where there are public images and private images. For this demo we’ll be using a public one, but if this is for your company you’d probably want to go with the private version.

Deploying Our Docker Rails App

Today we’ll be deploying our app to a pretty typical Linux box (Ubuntu, but it could be anything) which already has Docker and NGINX installed.

We’ll actually have to set up two other images (plus our Rails app, so three in total) on the server for the database. I’m going to be creating one image which is for storing DB data plus another for the DB server itself. Then we’ll run our main Alpine-Rails Docker image and link it to the DB server image. If you’re wondering why I’m doing it this way, Tim Butler has a great article on Linking and Volumes in Docker.

-e: Setting an environment variable which Rails will use. You may have to replace rake secret with an actual secret because the host server most likely won’t have rake installed

One important thing to note is that the database.yml file should be set up to point to our linked db-server image. To do this we can use the environment variables that the linked image exposes to us. To see a list of them you can run this command:

docker exec rails-server env

Two of the ENV variables listed will be something similar to the ones below. We can use these to point to our DB.

Lastly, you’ll probably need to enter the Rails console or to run rake db:create. To do so you can use the docker exec command on the server. docker exec rails-server bundle exec rake db:create

To run the Rails console you’ll have to pass the directives -it:

docker exec -it rails-server rails console

Configuring NGINX

If you visit the IP address of the server you won’t get any response yet. This is because our app is listening on port 3000. So instead of adding :3000 to the IP address, let’s configure NGINX to receive requests on port 80 and forward them to our app on 3000.

We’ll be using the upstream directive along with proxy_pass to hand the work off to another IP address and port, which happens to be the one for our Rails Docker container. This code would go inside of the http block in the NGINX config or in one of the sites-available conf files.

Conclusion

I’ll admit, getting comfortable with Docker can be a pretty steep learning curve. It does have some very powerful benefits though, not just for development but for deploying and running production code as well.

In this article we looked at doing the simplest deploy possible to Heroku. After that we went along more of a manual path, by hand crafting our own Dockerfile, building the image and deploying it to the Docker hub, and finally running this image on a production server.

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.