Setting up Docker development and production environments for Gatsby.

Download the repo

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:

  1. Define your app’s environment with a Dockerfile so it can be reproduced anywhere.
  2. Define the services that make up your app in docker-compose.yml so they can be run together in an isolated environment.
  3. 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

  1. docker-compose.yml for all the services and telling Nginx to serve the Gatsby build files
  2. docker-compose-dev.yml for the development environment using the Gatsby development server

Let’s start with the docker-compose.yml file first.

  1. Define your app’s environment with a Dockerfile. Done that!
  2. 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.