For a while I have been trying here and there to properly configure webpack-dev-server to serve the React frontend within a Rails app on my Cloud9 IDE workspace without much success. But eventually the frustration of not having the speed benefits of the live compilation made it a priority to fix. While I did find a lot of helpful information online (specially in a couple of GitHub issues, see sources below), the solutions mentioned did not quite work for me, probably because the underlying versions were not the same. In this article I explain how I got it finally working.

Note that the solutions in this article are for a React frontend within a Rails app. This is a bit different than a pure React app written only in JavaScript because the integration of the webpack-dev-server with the Rails app is ensured by the webpacker gem, which handles the configuration in a slightly different way. However, the approaches mentioned here might serve as a basis to solve it for the pure JavaScript case.

Note as well that this explanation is not about the Hot Module Replacement (HMR) that automatically updates the app in the browser without reloading. Instead it is about the live compilation of the JavaScript code that the webpack-dev-server offers, which is very fast compared to the on-demand compilation, which becomes slower when the size of the JavaScript codebase increases (see the webpacker documentation). In my case the on-demand compilation ended up taking more than 30s, which was highly inconvenient: i.e. every time I changed a JavaScript code in a React component, the server compiled the whole JavaScript code when I refreshed the app in the browser thus taking that long to respond.

After a preliminary remark about the proper binstub version of the ./bin/webpack-dev-server script, I will discuss two ways to tackle the issue: a simple and quick solution, which is sufficient if we work alone on a project, and a slightly more involved but flexible approach, that can be useful when several people might work in the same codebase.

Binstub versions

A lot of the confusion about the webpack-dev-server options and why they were not properly taken into account, was due in my case to an outdated version of the ./bin/webpack-dev-server script. Initially, the script I was using was created by the rails webpacker:install task of the webpacker gem v3.0.1 (source). However, I was using v3.0.2 (!!) of the gem (see full list of versions below), which logically expects the corresponding binstub version of the script (source). So please make sure that you are using the correct binstub (the same applies to ./bin/webpack). To be fair, the changelog of v3.0.2 properly mentions the change:

  • Added: Binstubs #833
  • (…)
  • Removed: Inline CLI args for dev server binstub, use env variables instead

Quick solution

If you are working alone, the easiest way to fix the configuration of the webpack-dev-server is to add an entry to the /etc/hosts and to modify the development.dev_server entry of the config/webpacker.yml file.

/etc/hosts entry

As mentioned in some of the sources below, you have to add an entry to /etc/hosts to alias the hostname of your Cloud9 IDE workspace to 0.0.0.0:

echo "0.0.0.0 ${C9_HOSTNAME}" | sudo tee -a /etc/hosts

Alternatively, since this has to be run every time your Cloud9 IDE workspace is restarted, you can add it to your .bashrc (and source it afterwards):

echo '(HOSTS_ENTRY="0.0.0.0 ${C9_HOSTNAME}"; grep --quiet "${HOSTS_ENTRY}" /etc/hosts || echo "${HOSTS_ENTRY}" | sudo tee -a /etc/hosts)' >> ~/.bashrc
source ~/.bashrc

config/webpacker.yml file

After trying many solutions mentioned in the sources below I ended up changing the development.dev_server entry of the config/webpacker.yml file from the following default values:

dev_server:
  https: false
  host: localhost
  port: 3035
  public: localhost:3035
  hmr: false
  # Inline should be set to true if using HMR
  inline: true
  overlay: true
  disable_host_check: true
  use_local_ip: false

into the these custom configuration:

dev_server:
  https: true
  host: your-workspace-name-yourusername.c9users.io
  port: 8082
  public: your-workspace-name-yourusername.c9users.io:8082
  hmr: false
  inline: false
  overlay: true
  disable_host_check: true
  use_local_ip: false

You can obtain the value your-workspace-name-yourusername.c9users.io for your Cloud9 IDE workspace with echo ${C9_HOSTNAME}.

There are three main differences with the approaches found in the mentioned sources:

  • Some solutions stressed the need to set the https option to false but this failed with net::ERR_ABORTED in the browser console and raised the following exception in the server when the client tried to get the JavaScript sources:

    #<OpenSSL::SSL::SSLError: SSL_connect SYSCALL returned=5 errno=0 state=unknown state>
    

    Setting https: true removes the issue.

  • By leaving the inline option to the default false value, the live compilation still works but the browser console constantly reports the following error:

    Failed to load https://your-workspace-name-yourusername.c9users.io:8082/sockjs-node/info?t=1511016561187: No 'Access-Control-Allow-Origin' header is present on the requested resource.
    Origin 'https://your-workspace-name-yourusername.c9users.io' is therefore not allowed access. The response had HTTP status code 503.
    

    Setting inline: false removes the issue.

  • None of the solutions I found suggested to set the public option in the config/webpacker.yml file and some suggested to pass it to the webpack-dev-server command line. By setting it in the configuration file we don’t need to care about it in the terminal.

With this configuration, running as usual ./bin/webpack-dev-server in one terminal and ./bin/rails s -b $IP -p $PORT in another did work for me. (Finally!)

Flexible solution

The previous solution is useful and fast to implement, but if you are working with other people on the same repo it can be tricky to maintain the proper configuration in the config/webpacker.yml file. Moreover, the hostname of your Cloud9 IDE workspace is hardcoded, so that the configuration is not portable.

I found a hint about another way to configure the webpack-dev-server in the webpacker documentation:1

You can use environment variables as options supported by webpack-dev-server in the form WEBPACKER_DEV_SERVER_<OPTION>. Please note that these environment variables will always take precedence over the ones already set in the configuration file.

However what I did not find in that documentation (but in the webpacker/dev_server.rb code) is that when the configuration of the webpack-dev-server is tweaked with ENV variables, those same values have to be passed to the rails server process as well in order to let it use the same configuration. This makes sense when you think about it, because if the configuration is defined in the config/webpacker.yml file, both processes can refer to it. However, if the configuration of one of the processes is overridden with ENV variables, the other one does not know about the new values.

With that in mind I finally got a flexible solution, using foreman with the following Procfile.dev:

web:        ./bin/rails server -b ${RAILS_SERVER_BINDING:-localhost} -p ${RAILS_SERVER_PORT:-3000}
webpacker:  ./bin/webpack-dev-server

and this bin/start script:

#!/bin/bash

# Immediately exit script on first error
set -e -o pipefail

APP_ROOT_FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
cd "${APP_ROOT_FOLDER}"

if [ -n "${C9_USER}" ]; then
  # We are in a Cloud9 machine

  # Make sure that Postgres is running
  sudo service postgresql status || sudo service postgresql start

  # Make sure that the needed entry in /etc/hosts exists
  HOSTS_ENTRY="0.0.0.0 ${C9_HOSTNAME}"
  grep --quiet "^${HOSTS_ENTRY}\$" /etc/hosts || echo "${HOSTS_ENTRY}" | sudo tee -a /etc/hosts

  # Adapt the configuration of the webpack-dev-server
  export APP_DOMAIN="${C9_HOSTNAME}"
  export RAILS_SERVER_BINDING='0.0.0.0'
  export RAILS_SERVER_PORT='8080'
  export WEBPACKER_DEV_SERVER_PORT='8082'
  export WEBPACKER_DEV_SERVER_HTTPS='true'
  export WEBPACKER_DEV_SERVER_HOST="${C9_HOSTNAME}"
  export WEBPACKER_DEV_SERVER_PUBLIC="${C9_HOSTNAME}:${WEBPACKER_DEV_SERVER_PORT}"
  export WEBPACKER_DEV_SERVER_HMR='false'
  export WEBPACKER_DEV_SERVER_INLINE='false'
  export WEBPACKER_DEV_SERVER_OVERLAY='true'
  export WEBPACKER_DEV_SERVER_DISABLE_HOST_CHECK='true'
  export WEBPACKER_DEV_SERVER_USE_LOCAL_IP='false'
fi

foreman start -f Procfile.dev

With these two scripts in place, the application can always be started by running bin/start, in both Cloud9 IDE and other systems. The trick is that by exporting the WEBPACKER_DEV_SERVER_* variables before calling foreman start, we make sure that those values are available to both webpack-dev-server and rails server processes.

Sources

I found valuable information and hints about how to fix the issue in at least the following resources:

Versions

Since things in this ecosystem move fast, it’s relevant to mention the versions of the world for which I got it working:

$ egrep '^    ?(ruby|webpacker|rails) ' Gemfile.lock
    rails (5.1.4)
    webpacker (3.0.2)
  ruby 2.4.2p198

$ yarn versions
yarn versions v1.1.0
{ http_parser: '2.7.0',
  node: '8.5.0',
  v8: '6.0.287.53',
  uv: '1.14.1',
  zlib: '1.2.11',
  ares: '1.10.1-DEV',
  modules: '57',
  nghttp2: '1.25.0',
  openssl: '1.0.2l',
  icu: '59.1',
  unicode: '9.0',
  cldr: '31.0.1',
  tz: '2017b' }

$ cat /etc/os-release | head -6
NAME="Ubuntu"
VERSION="14.04.5 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.5 LTS"
VERSION_ID="14.04"

Everything was tested using Chrome Version 62.

tl;dr

Change the development.dev_server entry config/webpacker.yml file into:

dev_server:
  https: true
  host: your-workspace-name-yourusername.c9users.io
  port: 8082
  public: your-workspace-name-yourusername.c9users.io:8082
  hmr: false
  inline: false
  overlay: true
  disable_host_check: true
  use_local_ip: false

Then run:

echo "0.0.0.0 ${C9_HOSTNAME}" | sudo tee -a /etc/hosts # execute after every restart

Now running as usual ./bin/webpack-dev-server in one terminal and ./bin/rails s -b $IP -p $PORT in another should work as expected. Make sure that you are running the proper binstub version of ./bin/webpack-dev-server.

  1. The usage of ENV variables to configure ./bin/webpack-dev-server is also mentioned in the changelog of v3.0.2, see “Binstub versions” section.