Setting up Docker development and production environments for Gatsby.
Docker is the Future
I love it. I’ve had experiences where it has taken hours to setup a new development environment on my Mac for an application requiring multiple services with their dependencies and configure it all to play nice on my OS. If I have to bring a new developer onto the team then I have to do this all over again working for hours walking through a complex series of steps to setup the new team member’s environment on their OS. Docker eliminates all that. All that is required to spin up a complete development environment with all the apps dependencies is to pull your app from the DockerHub repository or from your git repo, cd to the root of your project and launch your app from the terminal. Sweet…
What is Docker?
Quoting from Wikipeda:
Docker is a set of platform as a service (PaaS) products that uses OS-level virtualization to deliver software in packages called containers. Containers are isolated from one another and bundle their own software, libraries and configuration files; they can communicate with each other through well-defined channels. All containers are run by a single operating system kernel and therefore use fewer resources than virtual machines.
Why Docker?
Containers are a standardized unit of software that allows developers to isolate their app from its environment, solving the “it works on my machine” headache.
Once you build your Dockerized app it will work anywhere.
To install Docker go to the Docker installation docs and they will walk you through the process for your particular OS.
Along with Docker we will also be using Docker Compose.
What is Docker Compose?
Here’s an excerpt from the official Docker docs giving an Overview of Docker Compose.
Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration. To learn more about all the features of Compose, see the list of features.
Compose works in all environments: production, staging, development, testing, as well as CI workflows. You can learn more about each case in Common Use Cases.
Using Compose is basically a three-step process:
- Define your app’s environment with a Dockerfile so it can be reproduced anywhere.
- Define the services that make up your app in docker-compose.yml so they can be run together in an isolated environment.
- Run docker-compose up and Compose starts and runs your entire app.
What is Gatsby?
Gatsby weaves together ReactJS, GraphQL, webpack and more.
Since this is not a blog about Gatsby in particular I won’t go into the detail of the how and why of using Gatsby. Gatsby’s site will tell you all you need to know here.
Once you have Docker and Gatsby installed on your system, we can jump into the Dockerization of a simple Gatsby “hello world” starter site.
This chapter on setting up a Dockerized Gatsby environment is based on Aripalo’s elegant solution on Github.
As we proceed we will be drawing heavily on Aripalo’s elegant solution on Github on how to Dockerize Gatsby.
Create Our Project!
mkdir my-dockerized-gatsby
cd my-dockerized-gatsby
touch docker-compose-dev.yml
touch docker-compose.yml
touch Makefile
git init
Our file structure
-
my-dockerized-gatsby
- frontend
- docker-compose-dev.yml
- docker-compose.yml Makefile
Setup Gatsby
Install Gatsby hello world starter site
gatsby new gatsby-starter-hello-world https://github.com/gatsbyjs/gatsby-starter-hello-world
Rename Gatsby starter from gatsby-starter-hello-world to frontend.
mv gatsby-starter-hello-world frontend
cd frontend
Setup Dockerfile
cd frontend
nano Dockerfile
Add the following to our Dockerfile.
# /my-dockerized-gatsby/frontend/Dockerfile
FROM node:10-alpine
COPY ./entry.sh /
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN apk add --no-cache \
nginx \
python \
build-base \
&& npm install --global --no-optional gatsby@2.19.12 \
&& mkdir -p /www /run/nginx \
&& chmod +x /entry.sh \
&& ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
WORKDIR /www
EXPOSE 80
COPY . /www
RUN npm install
RUN gatsby build
ENTRYPOINT [ "/entry.sh" ]
CMD [ "serve-nginx" ]
You can skip the in-depth explanation and go directly to the entry.sh file.
What’s going on here? When we run docker-compose, (more on that later), the Dockerfile is going to build the Gatsby server.
Let’s walk through this step by step.
# /my-dockerized-gatsby/frontend/Dockerfile
FROM node:10-alpine
Using the FROM directive we pull our base Node.js Alpine image. Alpine is a lightweight Linux distribution and Node.js is a javascript server environment.
More about alpine-docker/node here.
# /my-dockerized-gatsby/frontend/Dockerfile
COPY ./entry.sh /
COPY nginx.conf /etc/nginx/conf.d/default.conf
We copy our entry.sh nginx files, (which we have yet to create), from the current directory to our node server on our Docker container.
# /my-dockerized-gatsby/frontend/Dockerfile
RUN apk add --no-cache \
nginx \
python \
build-base \
&& npm install --global --no-optional gatsby@2.19.12 \
&& mkdir -p /www /run/nginx \
&& chmod +x /entry.sh \
&& ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
Using the Docker RUN command we install the following packages
- nginx web server
- python
- build-base which is a meta-package that installs the GCC, libc-dev and binutils packages
&& run additional commands installing Gatsby and configuring the server
- install Gatsby from the npm software registry
- mkdir -p ** experiment with this mkdir -p /www /foo/bar
- chmod +x /entry.sh which makes the entry.sh file an executable
- create softlinks for messages and errors to access and error logs
# /my-dockerized-gatsby/frontend/Dockerfile
.
.
.
ENTRYPOINT [ "/entry.sh" ]
CMD [ "serve-nginx" ]
- ENTRYPOINT is a command telling docker to run the entry.sh script
- CMD [ “serve-nginx” ], Docker command setting the default parameter to “serve-nginx”
Create the /my-dockerized-gatsby/frontend/entry.sh shell script
touch entry.sh
nano entry.sh
Add the following to the entry.sh file.
#!/bin/sh
# /snippet_repo/frontend/entry.sh
set -e
export PATH=$PATH:/usr/local/bin/gatsby
if [ "$1" == "serve-nginx" ]; then
nginx -g "daemon off;"
else
gatsby $@
fi
What is going on in our entry.sh file?
if [ “$1” == “serve-nginx” ]; then
if the positional argument $1 is “serve-nginx” then do something… which poses the question when would the postional argument not be “serve-nginx”? Ok, so what are positional arguments?
What is going on here is if we are in the production environment serve the built Gatsby code then serve from Nginx.
If we are in the development environment then we use Gatsby to serve the code.
What determines which enviroment is set will be determined by the docker-compose files which we will build shortly, which will override the default Docker CMD.
3.4.1 Positional Parameters
A positional parameter is a parameter denoted by one or more digits, other than the single digit 0. Positional parameters are assigned from the shell’s arguments when it is invoked, and may be reassigned using the set builtin command. Positional parameter N may be referenced as ${N}, or as $N when N consists of a single digit. Positional parameters may not be assigned to with assignment statements. The set and shift builtins are used to set and unset them (see Shell Builtin Commands). The positional parameters are temporarily replaced when a shell function is executed (see Shell Functions). When a positional parameter consisting of more than a single digit is expanded, it must be enclosed in braces.
# /snippet_repo/frontend/entry.sh
nginx -g "daemon off;"
then nginx -g “daemon off”, what’s the difference between nginx daemon on and daemon off?
According to stack-overflow:
For normal production (on a server), use the default daemon on; directive so the Nginx server will start in the background. In this way Nginx and other services are running and talking to each other. One server runs many services.
For Docker containers (or for debugging), the daemon off; directive tells Nginx to stay in the foreground. For containers this is useful as best practice is for one container = one process. One server (container) has only one service.
Aha! Makes sense to me. 🙂
Create Nginx config file
Last piece of the puzzle for the frontend is the configuration Nginx to serve the Gatsby build files.
touch nginx.conf
Add the following to nginx.conf.
# /snippet_repo/frontend/nginx.conf
server {
root /www/public;
location / {
index index.html;
}
}
We are telling Nginx that root where our frontend build files live resides at /www/public.
/www/public? Where in the cyber ether does that exist?
Remember those empty docker-compose files we created earlier? Those docker-compose files will be our next step in the Dockerization of Gatsby. Onward!
Setup Docker Compose
- docker-compose.yml for all the services and telling Nginx to serve the Gatsby build files
- docker-compose-dev.yml for the development environment using the Gatsby development server
Let’s start with the docker-compose.yml file first.
- Define your app’s environment with a Dockerfile. Done that!
- Define the services that make up your app in docker-compose.yml so they can be run together in an isolated environment.
# /snippet_repo/docker-compose.yml
version: '3'
services:
frontend:
In the above we define our frontend service.
Next we need to tell Docker where the frontend Dockerfile lives which will commence the building of the frontend isolated environment.
# /snippet_repo/docker-compose.yml
version: '3'
services:
frontend:
build: ./frontend
Since our frontend service is running in a isolated environment we need to tell Docker to expose an external port that maps to the internal port that our frontend service is serving from. In this case that would be exposing port 8000 mapping to the internal port 80. The exposed port can be any port you like as long as the port number you choose doesn’t conflict with any of the other services that we will be running in the app.
# /snippet_repo/docker-compose.yml
version: '3'
services:
frontend:
build: ./frontend
ports:
- 8000:80
Specify a custom container name, rather than a generated default name and we’re done.
Here’s the finished docker-compose.yml file.
# /snippet_repo/docker-compose.yml
version: '3'
services:
frontend:
build: ./frontend
ports:
- 8000:80
container_name: frontend
Now we’re done with the docker-compose.yml file which is the production compose file used to fire up the built server-side-rendered Gatsby site.
Next we build docker-compose-dev.yml, the compose file to fire up the Gatsby development server.
In the docker-compose-dev.yml we need to override the docker-compose.yml file with additional service options:
- command
- volumes
Using command we can override the default command in the Dockerfile:
# /snippet_repo/frontend/Dockerfile
.
.
.
CMD [ "serve-nginx" ]
CMD [ “serve-nginx” ] # is the default command which tells the entry.sh file serve the Gatsby built files from Nginx. But when we are in our development environment we want to serve from the Gatsby development server.
Note the conditional in the entry.sh file, if $1 == “serve-nginx”. We are going to use the docker-compose.yml command service option to set a new default command develop –host 0.0.0.0 –port 80 which tells Gatsby to listen to requests from outside the docker container on port 80.
# /snippet_repo/frontend/entry.sh
.
.
.
# When the starting docker from the
# docker-compose-dev.yml file
# the $1 variable == develop --host 0.0.0.0 --port 80
if [ "$1" == "serve-nginx" ]; then
nginx -g "daemon off;"
else
# the $@ concatenates the docker-compose-dev command
# develop --host 0.0.0.0 --port 80 with gatsby
# which echos gatsby develop --host 0.0.0.0 --port 80
gatsby $@
fi
So, back to the docker-compose-dev.yml file, we add our command override.
# /snippet_repo/docker-compose-dev.yml
version: '3'
services:
frontend:
command: develop --host 0.0.0.0 --port 80
Next we use the volumes option to copy the frontend files over to the docker container.
Below is our completed docker-compose-dev.yml file.
# /snippet_repo/docker-compose-dev.yml
version: '3'
services:
frontend:
command: develop --host 0.0.0.0 --port 80
volumes:
- ./frontend:/www
Launch Our Dockerized Gatsby Site
To launch our Gatsby site in development mode, make sure that you are at the projects root then use the following docker-compose command
docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d
Note that we are invoking the production docker-compose.yml file and then overriding the docker production service options with our addition options from our docker-compose-dev.yml file.
To build and launch the production enviroment use the following command.
docker-compose up -d
After launching either enviroments you can use the following docker logs command to monitor the launch process which will give access to any errors if they should pop up.
docker-compose logs frontend
Build the Makefile
If you haven’t created a Makefile file in your project root, do so now.
touch Makefile
Add the following to your Makefile.
# /my-dockerized-gatsby/Makefile
build:
docker-compose build
prod:
docker-compose up -d
dev:
docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d
up-non-daemon:
docker-compose up
start:
docker-compose start
stop:
docker-compose stop
down:
docker-compose down
restart:
docker-compose stop && docker-compose start
restart-dev:
docker-compose down && docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d
restart-frontend:
docker-compose stop frontend && docker-compose start frontend
Now if you want to launch Gatsby from the development enviroment you can use the command make dev instead of the versbose docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d.
make dev
Or to launch in the production environment make prod instead of docker-compose up -d.
make prod
Voila! We are ready to develop with our new dockerized Gatsby environment.