Rails on Docker: Using Yarn to Manage Frontend Assets

Rails 5.1 introduced the front-end package manager Yarn into the standard tooling for new Rails projects. Yarn allows you to declare and manage your app's frontend dependencies (such as CSS frameworks and Javascript libraries) in a way similar to how Bundler manages Rails dependencies (gems).

If you're using Yarn to manage your app's frontend libraries, you'll need to ensure that Yarn is available to use when adding or precompiling your assets in your Docker container. To do this, you'll need to add a few extra lines to your app's Dockerfile.

Let's start by creating a new Rails app, and creating a basic Dockerfile for our app's image:

$ rails -v # Make sure you are using >= Rails 5.1
$ rails new my-app -d postgresql -B
$ cd my-app

Create your Dockerfile and add the following:

# /path/to/app/Dockerfile
FROM ruby:2.3-alpine

# Set local timezone
RUN apk add --update tzdata && \
    cp /usr/share/zoneinfo/Europe/London /etc/localtime && \
    echo "Europe/London" > /etc/timezone

# Install your app's runtime dependencies in the container
RUN apk add --update --virtual runtime-deps postgresql-client nodejs libffi-dev readline sqlite

# Install Yarn
ENV PATH=/root/.yarn/bin:$PATH
RUN apk add --virtual build-yarn curl && \
    touch ~/.bashrc && \
    curl -o- -L https://yarnpkg.com/install.sh | sh && \
    apk del build-yarn

# Bundle into the temp directory
WORKDIR /tmp
ADD Gemfile* ./

RUN apk add --virtual build-deps build-base openssl-dev postgresql-dev libc-dev linux-headers libxml2-dev libxslt-dev readline-dev && \
    bundle install --jobs=2 && \
    apk del build-deps

# Copy the app's code into the container
ENV APP_HOME /app
COPY . $APP_HOME
WORKDIR $APP_HOME

# Configure production environment variables
ENV RAILS_ENV=production \
    RACK_ENV=production

# Expose port 3000 from the container
EXPOSE 3000

# Run puma server by default
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

Notice the lines to install Yarn. This will install Yarn into your Docker image, and update the $PATH variable so that it is available to use on the command line:

$ docker build . -t my-app

Add Frontend Depedencies

Now your app's Docker image is built with Yarn installed, you can use it to spin up a container and add a frontend dependencies to your app.

To demonstrate this, let's add the Tachyons CSS framework:

docker run --rm -it --env RAILS_ENV=development --volume ${PWD}:/app my-app bin/yarn add tachyons

yarn add v1.3.2
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
├─ [email protected]
└─ [email protected]
Done in 2.10s.

If you look inside your app's folder, you'll see that yarn has added a yarn.lock file and a node_modules folder.

The lockfile is used by Yarn to determine exactly which versions of dependencies to install (similar to your app's Gemfile.lock for gems), and node_modules is where those dependencies are saved.

Note that the node_modules folder should not be committed to your code repository (it is add to .gitignore by default).

You should also ensure it is not added to you Dockerfile when building by adding it to .dockerignore:

# .dockerignore node_modules/

Using Frontend Dependencies

Rails 5.1 is configured to search the node_modules folder by default when precompiling assets. This means we can easily use the Tachyon's CSS framework by calling it in application.css:

/* app/assets/stylesheets/application.css */

/**
 * ...
 * = require tachyons/css/tachyons.min.css # <- Path relative to node_modules/
 * = require_tree .
 * = require_self
 */

(Note you can see the node_modules folder added to the asset pipeline in config/initializers/assets.rb)

Next, create a simple home controller that we can use the build a demo page:

$ docker run --rm -it --env RAILS_ENV=development --volume ${PWD}:/app my-app bin/rails g controller home index

And finally, add some Tachyons CSS classes to your app's layout. For now, we'll just create a simple red header across the top of the page:

    <!-- app/views/layouts/application.html.erb -->
    ...
    <body class="ma0">
      <header class="bg-dark-red ma0">
        <div class="mw8 center">
          <h1 class="f1 fw6 white ma0 pv3">Hello Rails</h1>
        </div>
      </header>

      <main class="mw8 center">
        <%= yield %>
      </main>
    </body>
    ...
$ docker run --rm -it --env RAILS_ENV=development --volume ${PWD}:/app --publish 3000:3000 my-app

With your app running, let's check that everything looks as expected. Open http://localhost:3000/home/index in your browser and you should be greeted with the (basic!) styled layout:

Hello Rails Tachyons CSS

Finishing Up

That's all there is to using Yarn! By installing it inside your app's Docker container, you'll also be able to successfully precompile assets when deploying your app to CI or production servers.

Yarn is called as part of the assets:precompile task to ensure that frontend dependencies are available to the asset pipeline. You can see this in action by running the task through your container:

$ rm -rf node_modules # Remove the existing node_modules folder first

$ docker run --rm -it --volume ${PWD}:/app --publish 3000:3000 my-app bin/rails assets:precompile
yarn install v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
Done in 0.72s.
I, [2017-11-24T10:12:57.634438 #1]  INFO -- : Writing /app/public/assets/application-dcfb333542166c7e176ac13945f5c27350627b932d212a870a5a112daf6d4db9.js
I, [2017-11-24T10:12:57.636782 #1]  INFO -- : Writing /app/public/assets/application-dcfb333542166c7e176ac13945f5c27350627b932d212a870a5a112daf6d4db9.js.gz
...