Dockerising Webpacker

An article following the creation of a dockerised Ruby on Rails application, complete with a dockerised Webpack development server running parallel.

A github repo containing the code discussed in this tutorial can be found on my GitHub

1. Introduction

This article will create a starter Rails application (running Ruby 2.7.1), dockerise it, and teach you how to run the webpack-dev-server (key for developing with JS frameworks such as React or Vue) in Docker. If you have a pre-existing & already Dockerised app then skip to Dockerising Webpacker.

For the sake of brevity I'm not going to cover installing Docker or Rails. If you are looking for guides to cover these, I can recommend the Docker and Rails official documentation.

Finally, I could not have written this post without the Rails on Docker book, to the extent that several lines I use are taken from here and adapted for purpose (with consent, and marked when used). If you're new to Docker I can't recommend it enough. Get started with a free course by the same author.

2. Getting started

We're going to need an application to dockerise. I'm sure you're familiar with this process, but we're going to take it up a level. Following the Rails on Docker guidelines, we're going to complete this entirely in Docker.

$ docker run --rm -it -v ${PWD}:/usr/src -w /usr/src ruby:2.7 sh -c 'gem install rails:"~> 6.0.3" && rails new --skip-test webpacker-on-docker-demo'
  > ...
  > Successfully installed rails-6.0.3.2
  > 40 gems installed
  > ...
  > Bundle complete! 14 Gemfile dependencies, 65 gems now installed.
  > rails webpacker:install
  > Node.js not installed. Please download and install Node.js https://nodejs.org/en/download/

If you're curious about the command above, here's a quick breakdown:

We'll get all the way to installing webpacker, and then hit an error. No worries, but to install Node and correctly install webpacker we're going to formalise our environment.

Now – there are a few more steps involved with setting up a modern rails app (ie installing webpacker) to run before we can get started. To start this process easier we're going to put together a Dockerfile and docker-compose.yml.

3. Dockerising the base application

We need to add two files to the root directory of our application:

  1. A Dockerfile to define our Docker image
  2. A docker-compose.yml to organise our containers, including our webpack one

Lets start with our Dockerfile – this is, if you're unfamiliar, a file created in the root directory of your app (alongside your Gemfile and .gitignore) with that exact name: Dockerfile. It dictates how to build our app's image.

# Dockerfile

FROM ruby:2.7

# Install nodejs
RUN apt-get update -qq && apt-get install -y nodejs

# Add Yarn repository
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

# Update
RUN apt-get update -y

# Install Yarn
RUN apt-get install yarn -y

ADD . /usr/src/app
WORKDIR /usr/src/app

# Install & run bundler
RUN gem install bundler:'~> 2.1.4'

RUN bundle

CMD rails server -b 0.0.0.0

This is code adapted from Chris Blunt's Rails on Docker, which provides an extensive introduction to Docker and the concept of containerisation

With it, you should be able to build your application:

$ docker build -t dockerising-webpacker-demo .
  > Step 1/11 : FROM ruby:2.7
  > ...
  > Successfully built xxxxxxxxxx
  > Successfully tagged dockerising-webpacker-demo:latest

Next, our docker-compose file. This is a yml document that organises and names our services to make them easier to manage.

# docker-compose.yml

version: "3.2"

volumes:
  dbdata:
    driver: local

services:
  db:
    image: postgres:11
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
      POSTGRES_USER: rails
      POSTGRES_PASSWORD: secret123
    volumes:
      - dbdata:/var/lib/postgresql/data/pgdata

  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      RAILS_ENV: development
      RACK_ENV: development
      POSTGRES_USER: rails
      POSTGRES_PASSWORD: secret123
    volumes:
      - .:/usr/src/app
    depends_on:
      - db

This is code adapted from Chris Blunt's Rails on Docker, which also covers concepts such as scaling containers with Docker Swarm. If you are interested in advancing your knowledge, I would recommend starting here

4. Installing Webpacker

With this written, we can run a command in a disposable container to install webpacker!

$ docker-compose build
  > ...
  > Successfully tagged webpacker-on-docker-demo_web:latest
$ docker-compose run --rm web bundle exec rake webpacker:install
  > Starting webpacker-on-docker-demo_db_1 ... done
  > create  config/webpacker.yml
  > ...
  > Webpacker successfully installed 🎉 🍰

... And finally to check that everything has worked as intended:

$ docker-compose up -d db
$ docker-compose up web
  > web_1  | => Booting Puma
  > ...
  > web_1  | Use Ctrl-C to stop

And navigate to localhost:3000 to finally, finally hit that classic welcome screen.

To give us something to use, I'm going to scaffold something very briefly:

$ docker-compose run --rm web bin/rails g scaffold user name:string
  > ...
  > invoke  scss
  > create    app/assets/stylesheets/scaffolds.scss

$ docker-compose run --rm web bin/rails db:migrate
  > == 20200619160457 CreateUsers: migrating ======================================
  > -- create_table(:users)
  >    -> 0.0077s
  > == 20200619160457 CreateUsers: migrated (0.0082s) =============================
# config/routes.rb

Rails.application.routes.draw do
  root to: 'users#index'
  resources :users
end

5. Dockerising Webpacker

If you've used webpacker (and it's webpack-dev-server) before, you'll know it runs on localhost:3035. Feel free to visit that now to see that it definitely is not running.

We're going to need to define a new service in our docker-compose file to run it. For clarity's sake, let's call it webpack:

# docker-compose.yml

webpack:
  build: .
  command: ./bin/webpack-dev-server
  volumes:
    - .:/usr/src/app
  ports:
    - "3035:3035"
  environment:
    NODE_ENV: development
    RAILS_ENV: development
    WEBPACKER_DEV_SERVER_HOST: 0.0.0.0

This will get the server running, but won't allow hot reloading... We can't have that. Additionally, if you've needed to restart your server for any reason you might be running into an error regarding leftover server.pid files. You can resolve this by manually deleting the temporary files, but I'm going to add a useful docker-entrypoint file to automatically resolve this.

# docker-compose.yml

services:
  web:
    ports: # Unchanged
    environment:
      # ...
      WEBPACKER_DEV_SERVER_HOST: webpack
    depends_on:
      # ...
      - webpack
    volumes: # Unchanged
# Dockerfile

... # Unchanged up to `CMD` line
CMD ./docker-entrypoint.sh
# docker-entrypoint.sh

#!/bin/sh

rm -f tmp/pids/server.pid
bin/rails server -b 0.0.0.0

And that should be all you need!

For the first run we're going to use docker-compose up web webpack to inspect the output from both containers. But, in the future, you can get away with docker-compose up web and the webpack service will run automatically :grin: You should get an output something like this...

$ docker-compose up web webpack
  > webpacker-on-docker-demo_db_1 is up-to-date
  > Starting webpacker-on-docker-demo_webpack_1 ... done
  > Starting webpacker-on-docker-demo_web_1     ... done
  > Attaching to webpacker-on-docker-demo_web_1, webpacker-on-docker-demo_webpack_1
  > web_1      | => Booting Puma
  > web_1      | => Rails 6.0.3.2 application starting in development
  > web_1      | => Run `rails server --help` for more startup options
  > webpack_1  | ℹ 「wds」: Project is running at http://localhost:3035/
  > webpack_1  | ℹ 「wds」: webpack output is served from /packs/
  > webpack_1  | ℹ 「wds」: Content not from webpack is served from /usr/src/app/public/packs
  > webpack_1  | ℹ 「wds」: 404s will fallback to /index.html
  > webpack_1  | ℹ 「wdm」: Hash: 4030c4049dd2c45d5d92
  > webpack_1  | Version: webpack 4.43.0
  > webpack_1  | Time: 5902ms
  > webpack_1  | Built at: 06/19/2020 3:45:00 PM
  > webpack_1  |                                      Asset       Size       Chunks                         Chunk Names
  > webpack_1  |     js/application-7ebe8ea7d1fee93ab92a.js    506 KiB  application  [emitted] [immutable]  application
  > webpack_1  | js/application-7ebe8ea7d1fee93ab92a.js.map    570 KiB  application  [emitted] [dev]        application
  > webpack_1  |                              manifest.json  364 bytes               [emitted]
  > webpack_1  | ℹ 「wdm」: Compiled successfully.
  > web_1      | Puma starting in single mode...
  > web_1      | * Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller
  > web_1      | * Min threads: 5, max threads: 5
  > web_1      | * Environment: development
  > web_1      | * Listening on tcp://0.0.0.0:3000
  > web_1      | Use Ctrl-C to stop

N.B Alternatively, you can use docker-compose up -d web (which will launch web in a detached state and webpack due to the depends_on), and then call docker-compose logs -f web webpack to see the output for any selected services

Items of note here:

To test that the webpack server is running successfully, you can check a few things.

  1. Navigate to localhost:3035. If you get an Unable to connect error, something is wrong. If you get a white page with Cannot GET / then, even though it looks like a disaster, your server is running successfully. This is because everything that should usually run through localhost:3035 is routed to localhost:3000 for us to work with instead.
  2. More importantly, test the actual reason we're here. Go to your app/javascript/packs/application.js and add a line – say, console.log('Hello from webpacker!'). Save the file, don't refresh your page, and it should update automatically.

From here, everything under the app/javascript/ directory will trigger a hot reload when its contents change!

Now, to put your newfound dockerised app to the test, try adding Vue components. Maybe something a little like this...