Use NGINX to Host a Local Site Using Docker

Use NGINX to Host a Local Site Using Docker

Sometimes, you can't simply open your index.html in Chrome to test your website. You need the features of a real web server like NGINX.

A common case where directly loading index.html falls short is trying to serve local files using the Fetch API. You run into Cross-Origin Resource Sharing (CORS) errors. I hit this little bump when creating the example for this blog post about the ClipBoard API. In addition to preventing CORS errors, it's more realistic to use a production web server in your development environment.

To get started, you only need to install Docker and run one command.

$ docker run -d --name nginx-development -v $(pwd):/usr/share/nginx/html -p 80:80 nginx

The above command does a few things. The -d flag ensures that you don't get stuck attached to the container. Using the --name nginx-development keyword argument gives this container instance a name, so we can reference it later. The -p 80:80 maps our local machine's port 80 to the container port that NGINX uses to serve HTTP. This port-mapping allows us to go to http://localhost to see our development site.

The -v $(pwd):/usr/share/nginx/html keyword argument needs a little more explaining. It links our current working directory to a directory in the running container instance. This directory in the container is the default location where NGINX looks for files to serve. If you want to serve a different directory, then you need to change the --volume argument. For example, I like to put my index.html at ./src/index.html in my project directory. To adjust for that, my -v argument would look like -v "$(pwd)/src:/usr/share/nginx/html".

If you use your browser to navigate to http://localhost, you should be able to see your site. But if you make changes to your index.html or any other files on your site, refreshing your browser won't show them. To reload the site after you make changes, use the following command.

$ docker restart nginx-development

It's good that we can reload our changes, but it requires the extra step: running the above docker restart command. With the previous workflow – opening up index.html directly – we only needed one step: refreshing our browser. Let's fix that 😤

Writing an Auto-Reload Script

We can write a simple bash script to reload the NGINX container whenever the source files change

To create this script, we need to either

  1. Poll the directory every so often and keep track of changes ourselves
  2. Have some way to be notified when there is a change

To avoid managing our own state and to keep our bash script simple, let's go with option 2. How can we get these notifications? On Linux, there is a command-line tool called inotifywait that relies on the underlying inotify C API. We could use this, but then we would be locked-in to Linux. I develop on Mac OS and Linux, so I'd like to support both – even better if we can support Windows too.

There is another tool called fswatch that works on Mac OS, Linux, and Windows. It may not be installed by default on your operating system. On Mac OS, I needed to install fswatch using the following command.

$ brew install fswatch

Now let's see how this tool works. Let's try running the tool and pointing it at our source directory.

$ fswatch src

My console just hangs with a blinking cursor. Let's see what happens if we modify one of the files in the src directory. I'll try editing one of my css files...

$ fswatch src
/Users/curtis/git/licorice-works/src/styles/4913
/Users/curtis/git/licorice-works/src/styles/projects.css
/Users/curtis/git/licorice-works/src/styles/projects.css
/Users/curtis/git/licorice-works/src/styles/projects.css~

Wow 😲 Almost immediately, fswatch gives us the above output with name of the file that I modified.

This is a lot of change notifications for modifying one file though. It looks like we don't want notifications for the 4913 file or for the .css~ file. These seem to be oddities of how saving files works on Mac OS. Luckily, the fswatch tool has some arguments that we can use to filter the change notifications. By default, it includes everything. This behavior makes including exactly the file types we want a little weird. First, we need to use -e '.*' to exclude everything. Then we can use -i 'css$' to re-include only files that end in css. With our new command, we only get notifications for the CSS file that we changed – not those other weird files.

$ fswatch -e '.*' -i 'css$' src                     
/Users/curtis/git/licorice-works/src/styles/projects.css
/Users/curtis/git/licorice-works/src/styles/projects.css

This is still two notifications though. We only want to reload NGINX once per change. Thankfully, fswatch has a --one-per-batch flag that we can use to batch the changes together.

$ fswatch -o -e '.*' -i 'css$' src
2

After adding -o, we only get one line of output when we modify a file. Let's use this output to trigger a restart of our Docker container.

$ fswatch -o -e '.*' -i 'css$' -i 'html$' -i 'js$' src | \
while read -r line; do docker restart nginx-development; done

That command looks more complicated than the commands we've used so far. Let's dig in. First, I added two more -i include filters. One to include any updates we make to .html files and another to include .js files. Then we pipe | the output of this command into a while loop. If you're not familiar with pipe, it just sends the output of the first command as input to the second command. The second command that we're piping into is a while loop. The general syntax of a while loop looks like the following.

while <condition>;
do
  <command to execute>
done

It's a little easier to read when it's not squished onto one line. For the <condition> part, we used a command read -r line. Whenever there is a line of input to read, this command reads it, stores it in the variable line, and returns true. In our script, this means whenever fswatch prints out a batch of changes, our loop will execute once. So each time there is a batch of changes, we will execute our restart command which is the same as above docker restart nginx-development.

You could get started by running the two commands.

$ docker run -d --name nginx-development -v $(pwd):/usr/share/nginx/html -p 80:80 nginx
$ fswatch -o -e '.*' -i 'css$' -i 'html$' -i 'js$' $(pwd) | while read -r line; do docker restart nginx-development; done

But this is a lot to remember or to copy-paste each time, so I wrote a script that takes only one argument: your source directory. You can copy this script and easily reuse it for each of your projects.

nginx-autoreload.sh

#!/bin/bash

CONTAINER_NAME="nginx-development"

# Use the first argument as our source dir.
# Default to current working directory if no argument is supplied.
SOURCE_DIRECTORY=$1
if [ ! $1 ]; then
    SOURCE_DIRECTORY="$(pwd)";
    echo "No source directory provided. Defaulting to current working directory: ${SOURCE_DIRECTORY}";
else
    # Get the full path
    SOURCE_DIRECTORY=$(realpath "${SOURCE_DIRECTORY}")
fi

docker rm --force "${CONTAINER_NAME}" &> /dev/null;

docker run -d --name "${CONTAINER_NAME}" -v "${SOURCE_DIRECTORY}":/usr/share/nginx/html -p 80:80 nginx > /dev/null;
if [ $? -ne 0 ]; then
    echo "Failed to initialize container";
    exit 1;
else
    echo "Successfully started container: ${CONTAINER_NAME}";
fi

echo "Waiting for changes..."
fswatch -o -e '.*' -i 'css$' -i 'html$' -i 'js$' "${SOURCE_DIRECTORY}" |
while read -r line; do
    docker restart "${CONTAINER_NAME}" > /dev/null;
    if [ $? -eq 0 ]; then
        echo "Successfully reloaded ${CONTAINER_NAME}";
    else
        echo "Failed realoading ${CONTAINER_NAME}";
    fi
done

You can use the script like $ ./nginx-autoreload.sh <your-source-directory, and it will take care of setting up the container and watching for changes. Note: remember to install Docker and fswatch first!

We did it! We wrote a single script to host and auto-reload your development site with NGINX 🥳