If you’ve ever struggled with using Docker to build front-end applications this tutorial is for you. This series of posts will demonstrate how to effectively use Docker to develop, build, distribute, and run a front-end application utilizing technologies such as React, Webpack, Babel, and Yarn.

If you’ve never used Docker before (or are a little rusty) you might want to start somewhere like here and then come back to this tutorial. This tutorial assumes you are already comfortable with Docker, running shell commands, and the basics of building and operating web applications.

In this first part of the series, I’m going to cover containerizing the build process for the front-end app, running the app using a web server (in a container of course!), and techniques to distribute the app for use in production.

Cat picture app demo

The demo app I’m using is a simple, single page application. I’ve built it without Docker during the initial iteration, using Yarn as the task runner and defining scripts in package.json . It’s a cat picture app that pulls random pics from The Cat API.

Exactly the kind of app this world needs.

Setup

If you want to follow along at home here’s a quick setup.

git clone https://github.com/slightlytyler/docker-ui-demo cd docker-ui-demo git fetch origin git checkout -b follow-part-1 origin/start-part-1 # and if you want to run yarn commands locally

yarn install

Building

Currently, the app is built like:

# after installing deps

yarn build

The goal now is to run this process inside a container. The end result should be a Docker image that contains the distributable files ( index.html , bundle.js , etc) and nothing else. This image isn’t runnable on its own and just serves as the distribution mechanism for the front-end assets during development (not at runtime). We’ll use this image later to build an example web server image that we can actually run.

Implementation

What does “containerizing” actually look like? First, we need to define the process of building the image using a Dockerfile .

Note: this uses multi-stage builds, a technique utilized here to help reduce the size of our final image.

We also need to add a .dockerignore as well so we don’t include local artifacts in our image.

Let’s break that down

FROM node:11.1.0-alpine AS node_base

The first stage defines our base image, the latest version of Node. This base image also includes Yarn.

FROM node_base as deps

WORKDIR /usr/app

COPY package.json /usr/app/package.json

COPY yarn.lock /usr/app/yarn.lock

RUN yarn install

The second stage installs our dependencies with Yarn.

FROM node_base as build

WORKDIR /usr/app

COPY --from=deps /usr/app/node_modules /usr/app/node_modules

COPY . /usr/app

RUN yarn build

The third stage copies our dependencies from the second stage and builds the distributable assets with Webpack.

FROM scratch AS ui

COPY --from=build /usr/app/dist /usr/app

In the fourth and final stage, we take a scratch (empty) image and copy over only the assets built during the third stage. Now we have an image that contains only the distributable assets for our front-end app.

Actually building

Now we can run a Docker build command to build and tag our image.

docker build -t slightlytyler/docker-ui-demo .

Further abstracting

Running Docker commands directly is OK but I’d much rather abstract away some of that complexity. We don’t want to make every developer remember that tag, right?

For that, we’re going to use GNU make and define a Makefile with a build target. This target will build our image and tag it with the current Git commit SHA and as latest.

Running make build , or just make , should build the image and tag it appropriately.

Building like a pro.

Running

Now that we have our built slightlytyler/docker-ui-demo image how do we actually run it? We’re going to use a Nginx image to build a simple web server, example code here. I’ve structured it as a separate project and Git repo, but you could easily do this in a different directory or even with a separate named Dockerfile .

Here’s the Dockerfile for the web server:

That’s it! This definition is very simple because all of the complexity of configuring Nginx to serve the SPA is built into socialengine/nginx-spa (check out the README). All we have to do is copy over the distributable files from slightlytyler/docker-ui-demo to the correct directory on the new web server image and everything works.

We can build the web server image:

docker build -t slightlytyler/docker-nginx-demo .

and run it:

docker run -it --rm -p 8000:80 slightlytyler/docker-nginx-demo

The app should now be available at localhost:8000 or port 8000 on whatever domain Docker is available on your machine.

Distributing

Now that we’ve built our images we need to push them up to a registry so that we can distribute our images to production environments and to other developers. It works like this:

A developer pushes up an image after building it. This is often performed by a CI system but could be done by the developer manually from their local environment. An operator pulls that image into a production environment where it is actually run. This is often performed by an orchestrator, like Kubernetes or Docker Swarm, but could be done by the operator manually from their remote environment.

There is no CI for this tutorial so I will just push the images up manually to Docker Hub.

docker push slightlytyler/docker-ui-demo docker push slightlytyler/docker-nginx-demo

If you want to do this part yourself you will need to change the tag var in the Makefile to your Docker Hub account’s namespace i.e. your-hub-name/docker-ui-demo .

Done 🎉 … with part 1

The build process is now containerized and we have a runnable image that serves the front-end application. The images are also pushed up to Docker Hub and are ready for distribution into production environments.

In my next post, I’ll show how containers can be utilized to make a development environment utilizing webpack-dev-server .

Next ➡️ Part 2 — Development environment