Dockerizing modern web apps
Tags: docker, webdev, devops
Published: 2018-09-19 08:09:31

---
title: Dockerizing modern web apps
published: true
description: These days everything is Docker. Let board the hype train and dockerize our websites.
tags: docker, webdev, devops
cover_image: https://thepracticaldev.s3.amazonaws.com/i/s529arx6n2aqepzgt02e.jpg
---

Most websites these days are Single Page Applications (SPA for short) where a single entry file handles all routes that a user might visit. Swept up in the ongoing trend of hosting in the cloud you might find yourself needing to “dockerize” your SPA. That is to say wrap it inside a Docker image and run it as a container.

![](https://thepracticaldev.s3.amazonaws.com/i/suyyg0olin0mx6ugrjzd.jpg)
<small class="figcaption_hack">These days everything is shipped in containers, even software.</small>

In this post we will explore how we can get that done. We are going to build a simple SPA that just tells the user which route of our website they are currently visiting. That means you should be able to visit not only `/` but also any route you might think of, such as `/unicorn` or `/rainbow`. This SPA will be a super simple and hand made one but you can see it as representative for any complex React, Angular or Vue app you might want to deploy. Finally we are going to build our SPA into a Docker image and deploy that as a container.

We will run through all the basics of what we are doing. So whether you are seasoned with Docker and just can’t get that SPA to run on your cluster or you are a great web developer tasked to do this Docker thing, this post is for you.

### The website

Our website will be super simple. Just a headline and a paragraph telling our user where they are by checking `window.location`. Below that we will offer links  to navigate to a few routes.

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Simple SPA</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body style="font-family: sans-serif;">
  <h1>Welcome to a simple SPA</h1>

  <p>You are on: <span id="locationSpan"></span></p>

  <p>You could go to:</p>
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/unicorn">Unicorn</a></li>
    <li><a href="/rainbow">Rainbow</a></li>
  </ul>

  <script>
    const span = document.querySelector('#locationSpan')
    span.innerHTML = window.location.pathname
  </script>
</body>
</html>
```

To test this locally you could initialize a package.json, install [live-server](https://www.npmjs.com/package/live-server) and add a start script to your package.json `"start": "live-server --entry-file=index.html"`. 

```bash
npm init
npm i -D live-server
# Now add the script o your package.json before running:
npm start
```

Go ahead and click on a few of the links to move around or enter another path into your navigation bar.

![](https://cdn-images-1.medium.com/max/800/1*l3FxcPMlOckHHpGZC8sXsw.png)
<small class="figcaption_hack">Our super simply SPA telling a user where they are and allowing them to
navigate.</small>

You might already notice that we are doing something to make our SPA work as we expect it to. We need to tell live-server to serve our index.html on all routes that it can't find a file for. We do this using `--entry-file=index.html`. Feel free to try running the  ive-server without the — entry-file parameter and see what happens in that case.

Keep this flag in mind as we will need to do something equivalent for our dockerization.

See the website in action at [dockerized-spa.now.sh](https://dockerized-spa.now.sh/). (Already dockerized and hosted on Now.sh.)

### Naive Docker attempt

[Docker](https://docs.docker.com/) is a system to create images which can then be run as containers. You can think of docker images as super lightweight Virtual Machines that can be run on many platforms (in this image containers are running VMs). The awesomeness of all this is that once you build a docker image and run it somewhere you will get the same thing running everywhere. Once we manage to build a Docker image locally that we can run as a container successfully we know that it will also run on AWS, GCP, Portainer or whatever else your company might be using.

> Docker solves the infamous “it works on my machine” problem. Containers run the
> same wherever you start them!

First you need a Dockerfile. Let us start out with a *Naive-Dockerfile*. Within it we will define the steps needed to create an image. In our case we just want an image that can serve websites and hold a copy of our Single Page Application.

```
FROM nginx

COPY index.html /usr/share/nginx/html
```

Here we base our image on the `nginx` image. [NGINX](https://www.nginx.com/) is a simple and lightweight webserver that can serve our *index.html*. To enable this we copy our website into the folder that NGINX will be serving. Now that we have this, let’s build our image and run it.


Above we first create the Docker image using [Docker build](https://docs.docker.com/engine/reference/commandline/build/). Using the `-f` Flag we tell Docker which “Dockerfile” to use, which files holdes the configuration to build our image. The `-t` flag “tags” our Docker image. It gives it a name we can use to run it.

Using [Docker run](https://docs.docker.com/engine/reference/commandline/run/) we then start the image as a container. By using `-p` we can specify a mapping for exposed ports, in this case that we want to reach the exposed port *80* at port *8888* on our local machines. So go ahead and open [http://localhost:8888/](http://localhost:8888/daf) to checkout what we got.

![](https://cdn-images-1.medium.com/max/800/1*8P6hCT8oQOhOqXt_ZePcwA.png)

![](https://thepracticaldev.s3.amazonaws.com/i/w0bdiudbddoio61axrxt.png)

Thats not how we imagined this would go…

With our “naive Docker” approach all but our entry route `/` display NGINXs 404 page.

```bash
docker rmi -f docker-spa
```

Lets clean up after ourselves using the [Dockerrmi](https://docs.docker.com/engine/reference/commandline/rmi/) command and `-f` forcing it to remove our created image. Time to pull up our sleeves and get this fully working.

### Empowering SPA capabilities

Remember how up above we needed to pass `--entry-file=index.html` to live-server in order for it to serve our *index.html* file for each route where it couldn’t find a file to serve? What we need now is the equivalent of this parameter for NGINX.

For that we will use an NGINX configuration and add it to our Docker image.

```
server {
    listen   80;
    listen   [::]:80 default ipv6only=on;

    root /usr/share/nginx/html;
    index index.html;

    server_name _; # all hostnames

    location / {
        try_files $uri /index.html;
    }
}
```

In the above config we tell NGINX to accept traffic on port *80* no matter the domain. Then we tell it to resolve paths ending in a slash as `index.html` and finally specify that for all routes it should check if there is a file and otherwise serve the index file.

After adding the above to our project we can now also COPY it into our Docker image to tell NGINX to use it.

```
FROM nginx

COPY nginx.config /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html
```

And once again you can test this locally using the same commands as above with our new *Dockerfile*. Remember to point your browser to [http://localhost:8888/unicorn](http://localhost:8888/unicorn) to see it in action.

```bash
docker rmi -f docker-spa
```

Clicking through the links you will see that now it does work. Each route you visit is now served by our Single Page Application we build at the beginning.

![](https://cdn-images-1.medium.com/max/800/1*GgY0vSpeKgif4-93-0s6IQ.png)

![](https://cdn-images-1.medium.com/max/800/1*PMs3fV3LMoFLxOzvHCI-yg.png)

### Bonus level — compiling your SPA inside Docker

Chances are your application isn’t just a single, static HTML file. In fact you probably have a quite modern and sophisticated toolchain involving TypeScript, Webpack, Parcel or similar to build your application. You could easily do this build step inside a Docker file.

```
# ---- Base Node ----
FROM node:alpine AS base
# Copy project file
COPY . .
# Build project
RUN npm run build

# ---- Prod ----
FROM nginx
# Copy needed files
COPY nginx.config /etc/nginx/conf.d/default.conf
COPY --from=base build /usr/share/nginx/html
```

The above uses a [multistage Docker build](https://docs.docker.com/develop/develop-images/multistage-build/). It first builds an image based on Node in which we run our build script and then builds an image as we did before but copying the compiled application form the
build image instead of our local file system where we run `docker run` from.

We can illustrate the usage of this with our simple SPA by adding a build script to our *package.json*.

```bash
"build": "rm -rf build && mkdir build && cp index.html build/index.html"
```

Go ahead and try it out.

```bash
docker rmi -f docker-spa
```

Take a look at all of this together in this [repo illustrating how to host SPAs using Docker](https://github.com/HoverBaum/super-simple-dockerized-spa).

### What a day

We built a Single Page Application, tested it locally, packaged it in a Docker image and finally enabled the Docker image to act as an SPA should, answering onall routes.

I hope you learned a thing or two today. Now, be brave, be bold, go out, apply your new found knowledge and host your application using Docker.

![](https://thepracticaldev.s3.amazonaws.com/i/28pv566f0zxb6u33evwf.png)

*****

[Repo with code](https://github.com/HoverBaum/super-simple-dockerized-spa)

[Live Demo on Now](https://dockerized-spa.now.sh/)