Deploy ClojureScript on Nginx
My first blog post outlined how to deploy a static ClojureScript website to Github Pages. This is an excellent choice for personal websites, or an open source project's documentation because you get free hosting infrastructure with relatively little effort. Having said this, what happens if you can't use static hosting providers like Github Pages or Netlify? What if you need to control more of the hosting service? This post will show you how to deploy your ClojureScript site on a webserver you control.
The Deploy Process
In order to make our site available to the internet we need a physical webserver
and a software webserver
. Once we have these, we just have to:
- Store your website's artifacts (
html
,css
andjs
) on aphysical server
- Teach your
software webserver
where those files live
In order to do the above, we need to perform the following steps:
- Build your website's production artifacts
- Move your website's production artifacts to a
physical webserver
- Configure your
software webserver
to deliver your website's artifacts to users
These steps are the same whether you are writing JavaScript
, ClojureScript
,
Reason
etc.
We can illustrate the whole process on your computer right now by:
- Using your as your
physical server
- Using Nginx as our
software server
- Using
ClojureScript
for our static website - Using docker to make the deploy consistent and predictable
Housekeeping
If you are going to follow along with this post please make sure you have the following tools installed:
Create App
Start by creating a basic ClojureScript app. The easiest way to do this is by following the templates getting started guide.
Once the above is complete you should have a project that looks like this:
.
├── README.md
├── deps.edn
├── dev.cljs.edn
├── resources
│ └── public
│ ├── index.html
│ └── style.css
├── src
│ └── demo_clojurescript_nginx
│ └── demo_clojurescript_nginx.cljs
└── test
└── demo_clojurescript_nginx
└── demo_clojurescript_nginx_test.cljs
Sanity Check Webserver
I always start with a sanity check. A sanity check is us setting a baseline to make sure things are working. If you were decorating a tree, this step would be us making sure the lightbulbs are working before we decorate the tree.
We're going to verify that we can use docker to pull and run an Nginx
container locally. To begin, pull
an nginx docker image into your local
filesystem:
docker pull nginx
We can verify that the image was successfully pulled in by running the following command
docker images
and if all went well we should see an Nginx
image in your local filesystem like:
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 540a289bab6c 3 days ago 126MB
If that went well, we can try to run our nginx docker container
docker run --name demo-clojurescript-nginx -p 80:80 -d nginx
and once again we will verify that the container is running
docker ps -a
and you should see something like
CONTAINER ID IMAGE COMMAND CREATED STATUS
9118c08ed0a6 nginx "nginx -g 'daemon of…" 6 seconds ago Up 4 seconds
and now you should be able to visit your docker container at
http://localhost:80
.
If the above is working we can move onto the next step which is deploying our app. The reason I'm manually going through these steps is so that we have a clear understanding of what we'll eventually automate.
Before we continue, make sure you stop your Nginx
docker container and remove
it like so:
docker stop demo-clojurescript-nginx \
&& docker rm demo-clojurescript-nginx
and let's start it again except this time we will start it on port 4001
.
docker run --name demo-clojurescript-nginx -p 4001:4001 -d nginx
Build Production Artifacts
This section is about building production artifacts. To clear things up, when I say artifact
I mean the code that will be run by our webserver. In the case of a web development project, the artifact
will be the minified and dead code eliminated JavaScript produced by the ClojureScript compiler.
To create this artifact we have to tell the ClojureScript compiler to build it for us. We do this with the following command:
clojure -M:prod
After the above command is run we can verify that we have a production artifact
by looking into the out
directory and finding a file called dev-main.js
out
├── cljs
│ ├── core
│ ├── core.cljs
│ └── core.js
├── dev-main.js
...more stuff
If that has worked, we can move to the next step: putting our production artifacts on the physical webserver
(docker container).
Move Production Artifacts to Webserver
This step is about creating a place for our production artifacts to live. To do this, we are going to manually go inside our nginx container and create the folder structure.
docker exec -it demo-clojurescript-nginx bash
and how create the folder like this:
mkdir -p var/www/app/cljs-out
and then exit the docker container. From here we can move the production artifacts from our local machine to the docker container.
# move the js artifact
docker cp out/dev-main.js demo-clojurescript-nginx:var/www/app/cljs-out/
# move the css artifact
docker cp resources/public/style.css demo-clojurescript-nginx:var/www/app/style.css
# move the html artifact
docker cp resources/public/index.html demo-clojurescript-nginx:var/www/app/index.html
From here, we just have to teach our webserver how to serve these files.
Teach Webserver to Serve Artifacts
At this point we just have to configure our Nginx
server to know where our production artifacts live. To do this, we have to remove the default Nginx
configuration file and replace it with a new one.
Start by execing into the Nginx
docker container:
docker exec -it demo-clojurescript-nginx bash
Remove the old configuration file:
rm /etc/nginx/nginx.conf
Then exit out of the docker container. At this point, we want to create a custom configuration file called nginx.conf
. This configuration file will live in the root of our ClojureScript app in a folder structure like tools/nginx/nginx.conf
inside the root of our demo-clojurescript-nginx
ClojureScript app. So go ahead and create the folder structure:
mkdir -p tools/nginx && touch tools/nginx/nginx.conf
and then you can open nginx.conf
and make it look like this:
events {}
http {
include mime.types;
gzip on;
server_tokens off;
server {
listen 4001;
root /var/www/app;
}
}
Now we want give this nginx.conf
file to our Nginx
container so it knows how and where to serve our project from. To do this, run the following command:
docker cp tools/nginx/nginx.conf demo-clojurescript-nginx:etc/nginx/nginx.conf
at this point we want to stop our docker container so the configuration file takes effect
docker stop demo-clojurescript-nginx
and then we can start the container again.
docker start demo-clojurescript-nginx
Now we should be able to visit our site at http://localhost:4001
and see the following:
Automate Process
Everything we just did is very manual and we can save time and reduce chances of human error by automating the above process. The following will show you how to do this.
Automate Prod Artifact Builds
Go into the tools/nginx
directory and create a file called Dockerfile.build
and make it look like this:
# base image
FROM clojure:openjdk-11-tools-deps-slim-buster
# create dir for our app to live in
WORKDIR /app
# copy the app from our local filesystem to the docker container app dir
COPY . /app
# build our prod clojurescript artifact
RUN clojure -M:prod
# The end. No need for a CMD to be specified because this is a build
# docker containers
A few things to note:
- we use
clojure
instead ofclj
- when you run this container you want to run it from the root of the app
- this will copy everything in the root project to a generically name folder called
app
This docker file is responsible for building our artifacts that we performed in step 2 earlier. Now lets go ahead and move to the root of our project and run that dockerfile and see if everything works:
docker build -t \
demo-clojurescript-nginx/build:0.0.0 \
-f "tools/nginx/Dockerfile.build" .
The above is going to build our image and the production clojurescript object. The next step is to run a container based off the above image so that we can get the production artifacts
docker run -d \
--name demo-clojurescript-nginx-build \
demo-clojurescript-nginx/build:0.0.0 \
sleep 20000
Move Production Artifacts to Local FileSystem
At this point, we want to copy the built files from the docker container to our local machine. If this were running in a CI/CD process we would have a special spot in the CI/CD environment for these temporary files. Because this is our local system, we have to create a temporary place for these things to live. Lets call this place temp/cljs-out
and we will create it in the root of our demo-clojurescript-nginx
repo.
mkdir -p temp/cljs-out
from here we can grab the files from our Dockerfile.build
container and move them to our temp dir like this:
docker cp demo-clojurescript-nginx-build:app/resources/public/index.html ./temp/index.html
docker cp demo-clojurescript-nginx-build:app/resources/public/style.css ./temp/style.css
docker cp demo-clojurescript-nginx-build:app/out/dev-main.js ./temp/cljs-out/dev-main.js
Automate Nginx Configuration
The next step is automate step 3 from above: nginx configuration and moving the artifacts from the above to the nginx container. To do this, we need to build our own nginx container which is going to be the docker container you would actually run. SO go ahead and create another docker container inside of tools/nginx
called Dockerfile
which looks like this:
# base image
FROM nginx:1.17.5
# remove the default nginx configuration
RUN rm /etc/nginx/nginx.conf
# add custom nginx configuration
COPY ./tools/nginx/nginx.conf /etc/nginx
# allow nginx conf to be executable
RUN cd /etc/nginx \
&& chmod +x /etc/nginx/nginx.conf
# create working directory
WORKDIR /var/www/app
# copy our temp artifacts
COPY ./temp /var/www/app
# start nginx container
CMD /bin/bash -c "nginx -g 'daemon off;'"
now build the image
docker build -t \
demo-clojurescript-nginx/prod:0.0.0 \
-f "tools/nginx/Dockerfile" .
and before we run our final image stop any instances of Nginx
docker containers you may still have running on port 4001
. e.g.
docker stop demo-clojurescript-nginx
docker stop demo-clojurescript-nginx-build
and now we should be able to run our Nginx
container
docker run -d \
-p 4001:4001 \
--name demo-clojurescript-nginx-prod \
demo-clojurescript-nginx/prod:0.0.0
Now we should be able to visit http://localhost:4001
and we should be able to see
Conclusion
And with that we have successfully deployed our ClojureScript app to our own Nginx
webserver. At this point, the next step is to deploy this on a server. Unfortunatley, that is out of the scope of this post, but if you are using a service like AWS or Google Cloud Platform they should have some awesome guides for deploying Nginx on their infrastructure.
Again, my hope is to outline a piece of the process. Let me know if this helped!