Ein Baum und seine Abenteuer

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.
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>
    <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" />
  <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>
      <li><a href="/">Home</a></li>
      <li><a href="/unicorn">Unicorn</a></li>
      <li><a href="/rainbow">Rainbow</a></li>

      const span = document.querySelector('#locationSpan')
      span.innerHTML = window.location.pathname

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

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.
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

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

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;

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

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.

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. 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"

Go ahead and try it out.

docker rmi -f docker-spa

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


Portrait picture of Hendrik

I am a JavaScript and GenAI Enthusiast; developer for the fun of it!
Here I write about webdev, technology, personal thoughts and anything I finds interesting.

More about me

Read next

Go based proxies for developing mobile websites on corporate WiFis

When networks become a show stopped for development.
When networks become a show stopped for development.

You might know this scenario:

We would really love to debug the web-app on an actual phone but they way our corporations WiFi is set up just won't allow it…

If you do, stay tuned because in this blog post we will examine how we as developers can handle tightly secured WiFi Networks and still get all the connectivity we need.

How I fell in love with an API-first CMS

Falling in love - Image by Contentful
Falling in love - Image by Contentful

The CMS (Content Management System) was one of the first building blocks of the content driven web. The CMS marked the move away from hardcoded HTML pages, and towards our modern web in which everyone has become a content creator. They are great for businesses because the competence of building websites and managing content could not only be split in theory, but also in practice. With a CMS, we can update our website on the fly — so there is really no justification for not using a CMS.

Holiday greetings with GenAI

Festive Greetings - ChatGPT and Midjourney
Festive Greetings - ChatGPT and Midjourney

Happy Holidays and festive greetings, powered by ChatGPT, Midjourney and a little bit of Photoshop.

Utilizing my Custom GPT for Midjourney prompts (open source on GitHub), I generated the image and some subtle variations in three rounds. Finally touching it up with a tagline in Photopea.