DEV Community

Cover image for Dockerizing modern web apps
Hendrik
Hendrik

Posted on • Edited on

Dockerizing modern web apps

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.


These days everything is shipped in containers, even software.

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.

<!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>
Enter fullscreen mode Exit fullscreen mode

To test this locally you could initialize a package.json, install live-server and add a start script to your package.json "start": "live-server --entry-file=index.html".

npm init
npm i -D live-server
# Now add the script o your package.json before running:
npm start
Enter fullscreen mode Exit fullscreen mode

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


Our super simply SPA telling a user where they are and allowing them to
navigate.

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. (Already dockerized and hosted on Now.sh.)

Naive Docker attempt

Docker 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
Enter fullscreen mode Exit fullscreen mode

Here we base our image on the nginx image. NGINX 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. 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 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/ to checkout what we got.

Thats not how we imagined this would go…

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

docker rmi -f docker-spa
Enter fullscreen mode Exit fullscreen mode

Lets clean up after ourselves using the Dockerrmi 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 to see it in action.

docker rmi -f docker-spa
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

The above uses a multistage Docker 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.

"build": "rm -rf build && mkdir build && cp index.html build/index.html"
Enter fullscreen mode Exit fullscreen mode

Go ahead and try it out.

docker rmi -f docker-spa
Enter fullscreen mode Exit fullscreen mode

Take a look at all of this together in this repo illustrating how to host SPAs using Docker.

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.


Repo with code

Live Demo on Now

Top comments (5)

Collapse
 
kr428 profile image
Kristian R.

Though I enjoyed reading through your article, I still wonder why deploying a single-page application requires docker. Isn't that a tad "overkill" for this solution? I mean, after all, a single-page application should be all HTML, CSS and JavaScript files that "just" need to be delivered through some static web server, and I don't even see much special nginx configuration in here. Is the effort of dealing with docker really worth it, in this case? ;)

Collapse
 
hoverbaum profile image
Hendrik

Glad you liked the article and very valid question. If you just look at hosting a website, even if it is a modern web app Docker is not what you would first come up with. Most providers have something dedicated for hosting static websites and they will also handle the SPA redirects to a single html file for you.

But people do come to the point where they want or need to host a website using Docker. This might be because a company wants all deployed artifacts to be of the same making or because for testing environments you only have Docker hosts or an environment to run a Docker container is more easily available to you or you want your backend team to be able to easily run the website on their machines. For all of these cases and more while Docker is not the native solution for hosting websites you might still want to do it this way.

Collapse
 
hoverbaum profile image
Hendrik

True point. For this super simple example as well as for most websites Docker is an overkill. You can usually get more benefits out of using a dedicated static hosting solution.

Though you can find yourself in situation where companies want to do everything in Docker just for consistencies sake.

All in all very valid point, though the point of this post is more about how to rather then should you do this 🙂

Collapse
 
sergsoares_15 profile image
Sergio Soares • Edited

Thanks for the post and repository with code. I agree that docker can be overkill in some situations but when work with SPA that can be builded for multiples environments (development, staging, production) with differents Env keys (Api key, Sentry Key, Analytics) to create images that represent different artifacts.