Running a Rails application in production requires installing Ruby and other packages. It's not difficult and configuration management system like Chef or Ansible makes it even easier. Here we're going to look at a different approach. By using Docker, installing Ruby and other packages happens on the build phase. In your production servers, you'll run your Rails application like any other Docker containers.
If you want to follow along, you need to have Docker installed on your local machine. I use docker-machine but there are other ways of getting Docker.
Before you run your Rails application in a container, you need to build a Docker image. This is done using a Dockerfile.
The focus of this blog post is to present a Dockerfile that produces a production-ready Docker image.
This Dockerfile starts from the ruby image and installs the necessary packages to run a Rails application.
FROM ruby:2.3 RUN mkdir -p /usr/src/app WORKDIR /usr/src/app RUN apt-get update && apt-get install -y nodejs mysql-client postgresql-client sqlite3 vim --no-install-recommends && rm -rf /var/lib/apt/lists/* ENV RAILS_ENV production ENV RAILS_SERVE_STATIC_FILES true ENV RAILS_LOG_TO_STDOUT true COPY Gemfile /usr/src/app/ COPY Gemfile.lock /usr/src/app/ RUN bundle config --global frozen 1 RUN bundle install --without development test COPY . /usr/src/app RUN bundle exec rake DATABASE_URL=postgresql:does_not_exist assets:precompile EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"]
We start by using the official ruby image. In this case we are using ruby:2.3. The 2.3 tag means the latest ruby 2.3 version which is currently 2.3.4. If you want to use ruby 2.4 you can use the image ruby:2.4.
We create our app directory at /usr/src/app and set our WORKDIR to that directory.
We install different packages that are needed by our rails app. Nodejs is used for assets. A db client library is necessary but you don't need all three. You can choose one among mysql-client, postgresql-client, and sqlite3. I like to install vim on my images in case I need to view config files on the container.
This Dockerfile is specifically for production so we need 3 environment variables. RAILS_ENV environment is set to production. RAILS_SERVE_STATIC_FILES is set to allow the app to serve static files which is otherwise disabled by default. RAILS_LOG_TO_STDOUT is set to log to standard out instead of a file.
We copy Gemfile and Gemfile.lock to /usr/src/app/ and run bundle install. We copy these 2 files separately before the rest of the app. Any changes on the source code, except for these 2 files, won't trigger another bundle install. Due to caching, if Gemfile and Gemfile.lock are not changed, bundle install will be skipped.
We copy the rest of the app.
We compile the assets. When running the rake task, DATABASE_URL is required and we pass a dummy value.
We expose port 3000 which is the port our app will use inside the container.
We run our app using rails server.
After saving the Dockerfile on your app's root directory, you can run docker build . -t your-dockerhub-username/image-name eg crigor/todo. The image will be created on your local machine.
If you do not have a Rails app to dockerize, you can use Engine Yard's todo app. Use the docker branch on my GitHub account at https://github.com/crigor/todo/tree/docker.
To push it to Docker Hub run docker push your-dockerhub-username/image-name. This step is optional. You will need to create an account on Docker Hub if you want to push your image.
You'll need a database with the todo app. There are a number of ways to run a database like postgres on your local machine. Here, our only requirement is that the database is accessible from the container. This gives you freedom on how to run postgres. You can use homebrew, the official postgres installer, or docker.
In production, you can use RDS, a separate server, or even Docker if you know what you're doing.
Back to my local machine, I use docker-machine on my Mac which runs a VM. I used the official postgres image from Docker Hub and forwarded port 5432 from the docker-machine VM to port 5432 on the container.
docker run -p 5432:5432 --name todo-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres
Run docker-machine env to get the IP of the VM. For me it's 192.168.99.100 which I'll use later.
Your docker image uses a RAILS_ENV of production but that doesn't mean you can't run it on your local machine. Your container needs 2 environment variables to work whether on your production server or your local machine. DATABASE_URL sets the database credentials and hostname of the database. SECRET_KEY_BASE is used for verifying the integrity of signed cookies.
On your local machine, run your app by passing the 2 environment variables on the command line
docker run -p 3000:3000 -e DATABASE_URL=postgres://postgres:firstname.lastname@example.org:5432/todo -e SECRET_KEY_BASE=69782b185cf994696b846e43b8e26a6c9f724905c74bf7556162c5a18cd17edc68a702ffbd0df7e855e2f4c6cf71bf68c794741c9234841f45446c3679bd8e6d your-dockerhub-username/image-name
In production, you may want to put your environment variables in a file and use docker run --env-file.
Putting credentials on environment variables is not secure though they definitely make things convenient. If you don't want to use environment variables, you can put your database credentials on config/database.yml and the secret key base on config/secrets.yml.
Create them on the server where you'll run your container and pass them to the container using volumes.
docker run -p 3000:3000 -v /path/to/secrets.yml:/usr/src/app/config/secrets.yml -v /path/to/database.yml:/usr/src/app/config/database.yml your-dockerhub-username/image-name
I've presented a Dockerfile which we used to build a production-ready Docker image for your Rails application. We tested this image on our local machine. This same image can be used to run your Rails application on your production servers. That's one of the great advantages of Docker. The same code that runs on your local machine will run in production.