Deploying Rails 6 Assets with Docker and Kubernetes

Our love for Rails, Docker and Kubernetes is no secret! In this post, I wanted to share some of our experience on how to deal with Rails 6 assets that use webpacker in Docker in a production environment.

What are we trying to achieve?

What do you need?

NOTE: If you use this example repository, all the files mentioned in this post are already created for you.

You’d also need to have Docker installed on your machine.
Basic understanding of using Docker command line and Dockefile format is also required.

While ultimately we are going to run this on a Kubernetes cluster (or Minikubes), the setup is the same and this post is going to focus on the tips of running Rails 6 in a containerized environment rather than focusing on the details of Kubernetes. For this reason, we’re going to use Docker Compose instead of Kubernetes to make things simpler as the Rails settings are the same in both environments.

Let’s get started

Running in Dev

To get going, create a file called Dockerfile in the application root directory like this one:

FROM ruby:latest

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update -qq && apt-get install -y build-essential nodejs yarn

ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

RUN gem install bundler:2.1.2
ADD Gemfile* $APP_HOME/
RUN bundle install

ADD . $APP_HOME
RUN yarn install --check-files
CMD ["rails","server","-b","0.0.0.0"]

Dockerfile

This installs the latest version of Ruby. If you want to change that, you can do so on the first line of the file. This might be needed if you explicitly have specified a ruby version in your Gemfile. Also here you see I am installing version 2.1.2 of Bundler gem. Feel free to use the version your Gemfile is compatible with (Rails 6 defaults to Bundler 2).

Our example app, like many others, uses MySQL as a database. To run both Rails and MySQL in docker on our laptop, we can use Docker Compose. Create a file called, docker-compose.yml in your application root directory like this one:

web:
build: .
ports:
- "3000:3000"
links:
- db
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_ADDRESS: db
RAILS_ENV: development
db:
image: library/mysql:5.6.22
ports:
- "13306:3306"
environment:
MYSQL_ROOT_PASSWORD: root

docker-compose.yml

Please mind that this is only for development purposes and that’s why you can see the password in clear text in this file!

To make sure Rails can see the database, we need to make sure our database.yml is configured correctly:

default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password: <%= ENV.fetch("MYSQL_ROOT_PASSWORD") %>

development:
<<: *default
database: rails6_mysql_development
host: <%= ENV['MYSQL_ADDRESS'] %>

test:
<<: *default
database: rails6_mysql_test
host: <%= ENV['MYSQL_ADDRESS'] %>

production:
<<: *default
database: <%= ENV['MYSQL_DATABASE'] %>
username: <%= ENV['MYSQL_USERNAME'] %>
password: <%= ENV['MYSQL_PASSWORD'] %>
host: <%= ENV['MYSQL_ADDRESS'] || ENV['MYSQL_HOST'] %>

database.yml

These is one last thing to do before we can run the app: create the database. This is needed only once unless you wipe the MySQL image off your laptop.

$ docker-compose run web rake db:create

Now we can start the app:

$ docker-compose up

On your laptop, visit http://localhost:3000 and you should see the app running.

Running in Production

To make this change, we’re going to add the following line to our dockerfile as the last line:

RUN RAILS_ENV=production bundle exec rake assets:precompile

This will run the asset precompilation and adds the compiled assets to the image.

Let’s run the application again, this time in production. To run the application in production, change the value of RAILS_ENV to production in your docker-compose.yml.

Visiting http://localhost:3000 you will notice none of the assets are served correctly. This is because Rails by default doesn’t serve static assets in production. This is done with the assumption that your web server (nginx, Apache, etc) is going to take care of the static assets before they hit the Rails stack to speed things up.

A note on secrets

  1. Generate a new secret by running rake secret copy the output
  2. Run rails credentials:edit --environment production and enter the value from step 1 as the value of the secret_key_base key in the file.
  3. Make sure RAILS_MASTER_KEY is passed in as a variable to your container. This is used by Rails to decrypt production.yml.enc file.

Once you close the editor, the content of the file will be encrypted and written to production.yml.enc under config/credentials. Rails also adds production.key to your .gitignore file to avoid leaking secrets into your git repo. You also need to make sure master.key is not commited into your git repo either, agian, by adding it to .gitignore file.

Subnote on VS Code and Rails Credentials Editor

$ export EDITOR=code -w

In case of VS Code, -w ensures the editor process stays up until the file is closed so it can be encrypted and written back to the disk.

Serving Static Assets in Production in Kubernetes

To make that change set RAILS_SERVE_STATIC_FILES to true in your docker-compose.yml and run the application again:

web:
build: .
ports:
- "3000:3000"
links:
- db
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_ADDRESS: db
RAILS_ENV: development
RAILS_SERVE_STATIC_FILES: 'true'
db:
image: library/mysql:5.6.22
ports:
- "13306:3306"
environment:
MYSQL_ROOT_PASSWORD: root

This time, running docker-compose up will start Rails and serves the static assets.

Where are my logs?

web:
build: .
ports:
- "3000:3000"
links:
- db
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_ADDRESS: db
RAILS_ENV: production
RAILS_SERVE_STATIC_FILES: 'true'
RAILS_LOG_TO_STDOUT: 'true'
db:
image: library/mysql:5.6.22
ports:
- "13306:3306"
environment:
MYSQL_ROOT_PASSWORD: root

Now you should see your logs!

Summary

  1. Make sure yarn is installed in your Docker image (see the Dockerfile example above)
  2. Install Bundler 2 and above. Most Docker Ruby base images come with Bundler 1.
  3. Run yarn install --check-files in your Dockerfile
  4. Run RAILS_ENV=production bundle exec rake assets:precompile in your Dockerfile.
  5. Set RAILS_SERVE_STATIC_FILES to true
  6. Set RAILS_LOG_TO_STDOUT to true if you would like to see the logs.
  7. Make sure you either have a credentials file in production or set the SECRET_KEY_BASE variable in production and during the image build phase.

Can it be made simpler?

Originally published at https://blog.cloud66.com on January 31, 2020.

DevOps-as-a-Service to help developers build, deploy and maintain apps on any Cloud. Sign-up for a free trial by visting: www.cloud66.com