Webpack and Docker for Development and Deployment

Updated on July 6, 2017: Thank you for all your interests in this story. When I wrote this article, I didn’t thought that it will help many to solve their problems. Please take note that Webpack and Docker have since developed to newer version, and codes and samples in this article might not work well. However, I hope this article can still help you to learn something new. =)

Webpack and Docker are great for both development and deployment processes.

When using Webpack for development, you get great dev tool support (read: webpack-dev-server and hot reloading) and can use any existing Javascript library without any efforts. Webpack can even replace Browserify. When using Webpack for deployment, Webpack helps you to bundle your modules (e.g. CSS, JSX, JS, etc. ).

When using Docker for development, as pointed out by Mark Wolfe, you get

  1. Portable build environment
  2. Simplified on-boarding of new developers
  3. Consistency between development and continuous integration (CI)

Here is another article that talks about React and Flux: A Docker Development Workflow by Aaron Tribou. When using Docker for deployment, you can ship your codes easily.


Context

When doing my side project, I was trying to put Webpack and Docker together so that I can enjoy those advantages. I found out that the process is awful; all pieces are shattered everywhere in the Internet. That’s why I am writing this article.

We will start by looking at a small and simple React application, evolve the codes to use both Webpack and React, and along the way, look at the problems and their solutions.

All the code can be found in this Github repository. You can code along with me, or browse through tags in this repository. I will give you tags along the way.

Let’s Start Coding

First, we follow tutorial by Blake Williams to set up a small react application with Webpack.

// webpack.config.js (tag: v1)
...
module.exports = {
context: contextPath,
entry: {
javascript: ‘./index.js’,
html: ‘./index.html’
},
output: {
path: buildPath,
filename: ‘bundle.js’,
},
...
}

We can then run our webpack-dev-server:

$ webpack-dev-server --hot --inline

Webpack-dev-server is great to development purpose, but you would not want to use it for production for obvious reasons. We then follow tutorial by christianalfoni to set up an Express.js server to proxy requests to webpack-dev-middleware and serve static content in production (You might be able to set up a Nginx server to serve static content, but I will not discuss it here).

// server.js (tag: v2)
import express from ‘express’;
const isDeveloping = process.env.NODE_ENV !== ‘production’;
...
if (isDeveloping) {
let webpack = require('webpack');
let webpackMiddleware = require('webpack-dev-middleware');
let webpackHotMiddleware = require('webpack-hot-middleware');
let config = require('./webpack.config.js');
...
// serve the content using webpack
app.use(middleware);
app.use(webpackHotMiddleware(compiler));
...
} else {
// serve the content using static directory
app.use(express.static(staticPath));
}
...

In this small Express.js server, we serve the content using Webpack in development environment, and we serve static, pre-generated contents in production environment. We import webpack modules only when we are in development environment. Development and deployment become much easier now.

# For development
$ node index.js
# For production
$ webpack
$ NODE_ENV=production node index.js

Next, we will dockerize this application. For your information, npm install will run very slow when building the docker images. To speed up the build process, blackjackcf mentioned in this Github issue that:

I found what helped was caching the npm install layer and pointing npm to a registry (I use a private repository, but I found just using npm registry speed things up too)
# Dockerfile (tag: v3)
FROM node:0.12.7
RUN npm install webpack -g
WORKDIR /tmp
COPY package.json /tmp/
RUN npm config set registry http://registry.npmjs.org/ && npm install
WORKDIR /usr/src/app
COPY . /usr/src/app/
RUN cp -a /tmp/node_modules /usr/src/app/
RUN webpack
ENV NODE_ENV=production
ENV PORT=4000
CMD [ “/usr/local/bin/node”, “./index.js” ]
EXPOSE 4000

We also write a .dockerignore file to ignore node_modules/ and public/ folders. We ignore these folders to avoid machine dependent codes to be copied to our container. We can now run our codes in container:

$ docker build -t webpack-docker .
# For development
$ docker run --name my-webpack-docker -p 80:4000 -e NODE_ENV=dev webpack-docker
# For deployment
$ docker run --name my-webpack-docker -p 80:4000 webpack-docker
# To view the application, you need to know the ip address of your virtual machine
$ docker-machine ip default

Now, we need to build and run the images every time when we change our codes for both development and deployment, which is tedious. We lost Webpack hot reloading feature at the same time. When we change our codes in host machine, the changes are not reflected in the container.

We will solve this problem by mounting a volume (of our code folder) to the container. As mentioned by RDX in this article, naively mounting your codes folder will cause the node_modules/ folder goes away (If you already called npm install command in your host machine, then machine dependent node_modules/ folder will be mounted to the container).

# docker-compose.yml (tag: v4)
web:
build: .
ports:
- “80:4000”
volumes:
- .:/usr/src/app/:rw
environment:
- NODE_ENV=dev
# Before you run this file for the first time, make sure
# you remove node_modules/ folders in your host machine
command: >
sh -c '
if test -d node_modules;
then
echo node_modules_exists ;
else
cp -a /tmp/node_modules /usr/src/app/website;
fi &&
npm install &&
/usr/local/bin/node ./index.js
'

In docker-compose.yml, we mount current directory of host machine to /usr/src/app directory of container. All files and subdirectories that have been copied during the docker build process will be replace with the mounted volume.

We also override the default command in the Dockerfile. If the node_modules/ folder does not exist in current directory of host machine, we copy the pre-install (during docker build) node_modules/ folder from the container to current directory of host machine. To run a development docker container with hot reloading:

# For development
$ docker-compose up

It turns out that copy, cp and remove, rm command are very slow in docker-compose. After the first time running the development container, we assume that the node_modules/ directory contains codes that depend on the container environment, not the host environment. So when node_modules/ directory is present in current directory, we will just go ahead to run the application.

If you make changes to your React component (such as the one in src/greeting.js), the changes will be reflected immediately in your browser. We finally able to use Wepack and Docker in both development and production environment!


Wrap Up

We started by looking at a small React application with Webpack. We then wrote an Express.js server as a proxy for Webpack bundling. Next, we wrote a Dockerfile for production environment and a docker-compose.yml file for development. You should observe that along the way, we simplified the process of setting up a new environment; from webpack + npm install + node to just docker-compose up.

By now you might ask, since working with Webpack inside Docker is hard, why would we still do that? Because our life would be much difficult without it! In my side project, I need to set up environments by installing many machine dependent library (such as gRPC and Protocol Buffer 3) before I can run the project. Docker saves my time! I already can imagine the momment I pass my project to the next person, I can handsomely tell him to run my project by using docker-compose up command!