I'm probably late to the game, but I have just discovered Docker's new (well..) feature, the multi-stage builds. At first, it came handy for building Go binaries, starting with a golang base image, compiling the project and then continuing with a scratch image to actually run the binary. Here's how it helped me build the containers for the Discover project. Superb!

But then I started thinking about other cases and suddenly it struct me! Frontend baby! In this article I will go through building a Dockerfile suitable for holding a Gatsby project. This Dockerfile will be able to serve a development environment with the help of docker-compose, but also creating a final image from nginx ready to go up on your kubernetes cluster (or wherever really).

So, let's get on.

The process

In a frontend project there are usually two distinct processes. The development and the build. Development will spin up a local server, probably with webpack, some file-watching daemon, etc. The build process will build everything up producing the final artifacts that will go on your server. create-react-app anyone?

The base in each of these processes is the same. Install Node, fetch npm packages, and so on.

Gatsby in particular, has two commands, gatsby develop and gatsby build.

The Dockerfile

Let's start with the base image. Here's a very common Dockerfile for building a Gatsby project.

FROM node:10 as node

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm ci

COPY . .

EXPOSE 8000

CMD [ "gatsby", "build" ]

Pretty basic.

Now let's add a docker-compose.yaml file to help us with local development. You may have one of these already probably serving a local API, so embedding it into your workflow will be easy peasy.

version: "3.4"

services:
  website:
    container_name: gatsby_website
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - 8000:8000
    command: ./node_modules/.bin/gatsby develop -H 0.0.0.0
    volumes:
      - /usr/src/app/node_modules
      - .:/usr/src/app
    environment:
      - NODE_ENV=development

Notice how we are overriding the command so instead of running gatsby build inside the container, the gatsby develop process will kick in instead. Try it by running running docker-compose up. The local service should start and you will be able to make any changes and watch them go live.

The deployment

But now, we would like to actually build our website and put it inside an nginx container. That container will then be deployed in a kuberentes cluster. Let's do some modifications to our files above:

FROM node:10 as node

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm ci

COPY . .

CMD [ "gatsby", "build" ]

+ FROM nginx as server
+ 
+ EXPOSE 80
+
+ COPY --from=node /usr/src/app/public /usr/share/nginx/html
version: "3.4"

services:
  website:
    container_name: gatsby_website
    build:
      context: ./
      dockerfile: Dockerfile
+     target: node
    ports:
      - 8000:8000
    command: ./node_modules/.bin/gatsby develop -H 0.0.0.0
    volumes:
      - /usr/src/app/node_modules
      - .:/usr/src/app
    environment:
      - NODE_ENV=development

So now I've added a second stage to our Dockerfile that starts from nginx and also copies all the artifacts from the previous stage. docker-compose has also been accommodated to stop at the first stage so it will never reach the second one.

Let's build the image now with Docker:

> docker build -t gatsby-image .

That's it! Now our Dockerfile will produce an nginx container with our final website deployed in. docker-compose will continue to work as. Brilliant!

Conclusion

And there you go. A single Dockerfile to use for both development and production in conjunction with docker-compose. Life just became simpler.

I'm sure more use cases can come out of that. I would love to hear how are you using it! Hit me in the comments below.