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 😤
To create this script, we need to either
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 🥳
Also published here.