Containerizing A GatsbyJS Site With Docker Compose
Saving yourself the pain of deploying your site with Docker
Above is a real-life re-enactment of a developer having a mental breakdown trying to deploy their website without Docker /s
Why Containerize Your Gatsby Application?
Ease Of Development & Deployment
Anyone who has had to work with other developers working with different Operating Systems knows the strange bugs and issues that come up due to OS-specific issues.
Furthermore, managing the dependencies and configurations needed for production deployment can be extremely time-consuming.
Docker fixes these issues by:
- Making it easy to get your application running, with all of the dependencies, locally with Docker Compose
- Declaratively define the system dependencies and the infrastructure configuration for part of your application.
- Allowing you to isolate portions of your application into containers and optimize each container according to its needs (i.e configuring NGINX with the Gatsby container but not for others).
- Easy integration with cloud providers and CI/CD pipelines.
There’s also the benefit of the ecosystem of available Docker images, that allow you to start from good baselines, in Docker Hub.
You can learn more about the history of containers here: Why use Docker?
Ease Of Integration With Complex Web Systems
Due to the aforementioned configurability of the containers, and the power that Docker Compose allows, its easier to isolate different services of your application and integrate them together.
Docker Allows You To Bake In All Your Configurations
For instance, you could have an NGINX router that passes requests either to your Gatsby client or API depending on the request type. Then serving your Gatsby client with another NGINX server that is configured to optimize serving static files. (Such as leveraging Brotli compression)
This application utilizes the following containers:
- NGINX router container
- API container
- Gatsby client container
Which can be connected together via Docker Compose. Instead of having to create the NGINX setup via terminal on your production/development/testing server, you can bake them into the containers.
You can learn to move about docker-compose here.
Docker Makes It Easy To Scale
This containerization of all pieces of your application allows you to easily test and deploy them as part of a CI/CD pipeline. Especially considering the support Docker has across all cloud providers.
And if your application has intense requirements and needs to be highly scalable, Docker allows you to leverage Kubernetes. Kubernetes allows an even more powerful composition and management of the services.
Building Out The Foundations
Prerequisites
- Node (Should include NPM)
- Docker
- Gatsby CLI
If you already have these installed you can skip to the Gatsby Overview section.
Installation
The following section focuses on going through the installation steps of everything we’ll need for our purposes, walking through the options available in all of the major operating systems.
Windows
You can download Node and NPM either by stepping through the wizard form on Windows installing packages that allow you to manage the specific versions of Node and NPM you’re using.
There are two major packages that allow you to achieve this in Windows:
Then, you will have to install Docker on the computer you will be working from. On windows, you can install the Docker Desktop application from the Docker Docs.
Note On Docker For Windows
The installation of Docker for Windows requires the user to have the paid version of Windows, which many developers do not have or want. In this case, there are three options available to you to set up Docker in your computer:
- Virtual Box — You can setup Virtual Box in your computer, which allows you to run a virtual machine on your windows machines that run a version of Linux. This allows you to run the Linux version of Docker.
- Dual Booting — Alternatively, if you plan to use Docker more extensively and for most of your projects, it may be more beneficial to set up dual booting on your computer.
- WSL 2 — The final option is to set up WSL 2 on your computer, which is an involved process but slightly easier than Dual Booting. You can follow this guide released by Microsoft to set up WSL 2 on your computer.
Mac and Linux
In Mac, you can also step through the wizard form similarly to on windows. But you can also install a node version package manager.
UNIX-based systems (such as Mac and Linux), you have available to another package manager not on windows.
Then you will have to install Docker, which depends heavily on the specific OS and distribution that you have.
- Mac: If your working computer is a Mac, I would advise installing the Docker Desktop application, it will be easier to get set up and working.
- Linux: If your working computer is a Linux, you can follow the installation from source steps for your specific Linux distribution.
Unlike Mac and Windows, you won’t have a Docker Desktop application, and will only have the Docker server(which offers the same functionality simply without the GUI).
Gatsby Overview
Once you have Node and NPM installed on your computer, you can install the Gatsby CLI by running the command:
npm install -g gatsby-cli
Although we will run the actual Gatsby server on a Docker container, we will use the CLI to generate a basic site from the available Gatsby Starters.
Cloning The Gatsby Starter
We will be working off the default Gatsby Starter, which you can preview here.
First, we’ll create a folder where we’ll store the entire application and then generate the Gatsby Starter within that folder.
Open your terminal, move to your preferred working directory, and type in the following commands.
Base Folder & Clone The Gatsby Starter
mkdir docker-gatsby
gatsby new client https://github.com/gatsbyjs/gatsby-starter-default
You can test to see if the installation went properly by running the following commands and you should see the default starter on localhost:8000
cd client
npm install
npm start
Development Container For GatsbyJS
For our purposes we want our development container to cover allow all of the features that Gatsby provides when running locally and keep the container as small as possible.
So we’ll make sure follow these guidelines
- Use the minimal base Docker image necessary
- Download all the system requirements necessary to run Gatsby
- Allow for live updating of the code
Before we start that you’ll have to make a small change to the develop script your package.json.
Change the script to this:
"develop": "gatsby develop -H 0.0.0.0"
The -H configuration sets to host number that the Gatsby server will be listening on.
The default host is localhost(127.0.0.1), but since our Gatsby server will be inside a docker container, it won’t be accessible to the outside world.
Instead, the host number 0.0.0.0 will allow us to be able to access the Gatsby server running inside the container. Similarly, it’ll also allow other Docker containers to interact with the Gatsby Server. You can read more about the difference between localhost and 0.0.0.0 here.
Here’s the development Dockerfile that we’ll serve our needs, we’ll walk through each part and what it achieves.
Dockerfile.dev
FROM node:alpine # The minimal baseline we need for Nodejs
WORKDIR /app # COPY the package.json file, update any deps and install them
COPY package.json .
RUN npm update
RUN npm install # copy the whole source folder(the dir is relative to the Dockerfile COPY . . CMD [ "npm", "run", "start" ]
You can read more about the differences between using the RUN command and the CMD command here.
We’ll pair the above Dockerfile with docker-compose for easy management of all of the Docker containers we’ll see for our application.
docker-compose.yaml
version: "3"
services:
client:
build:
context: ./client
dockerfile: Dockerfile.dev
volumes: # Links the source files to the running container
- ./client/src:/app/src
ports: - "3000:8000"
Build
The build configuration allows you to directly specify how you want Docker to pose to build the container. In the above example, we give it a context and the container docker file.
The context specifies the working directory for that service. The Dockerfile option allows us to specify the filename(including extension) where we defined our Docker image.
Volumes
The volume section will link the src folder in the Docker container with your local version. This allows you to create live changes without having to restart your container.
Important Note On Volumes
There are a lot of different ways to set up a volume for development, with different trade-offs.
The one in the above only links the source files, so when you want to install a new package you will have to rebuild the container.
To do this you can simply run:
docker-compose up --build
This is because the package.json and every other file aren’t linked, and so any updates will not be reflected on the running container.
If you choose to instead make the whole client directory linked to the container, the package.json will be updated.
But in order to utilize the installed package, you will have to run “npm install” inside the running container.
You can do this by running
And then executing npm install within that shell.
Why Only Link the Source
After trying every different approach I could find, ultimately for development, I decided to only link the source code for a few reasons.
- The reason we had the volumes has so we could update the running container, which the vast majority of the time only involves the src/ folder.
- When updating other Gatsby specific files, you’re still going to have to restart the Gatsby server which means you’ll have to stop the Docker container.
- Linking the whole client directory results in the .cache, and public folders to be mirrored locally. Which can also cause strange issues when stopping a container that results in dozens of cache files to have to be deleted individually
- By only linking the source file, you can have the development dependencies downloaded locally (which can be used by vscode/other text editors) without interfering with your running container or vice-versa.
There are valid reasons why you’d want to configure your volumes differently.
But I’ve found that the above configuration balances developer experience and best practices quite well when developing Gatsby applications.
If you think you’d found a way to configure your Docker Compose in a smoother and more intuitive way, let me know! Either below in the comments or reach out to me personally.
Port Mapping
The ports section will take map the internal ports inside with the Docker container with externally accessible ones. So in our instead, we will access our Gatsby application through localhost:3000.
Running Development
Once you have all of your files set up, you can start your containers with the following command:
docker-compose up
Or if you need to rebuild your Docker image (the Dockerfile.dev)
docker-compose up --build
The first time you build the docker image and run docker-compose up, it will take a very long time. But every time afterward will be magnitudes faster.
Production Container For GatsbyJS
A good heuristic when developing Docker containers is to assume that your host machine isn’t able to either install the application’s dependencies or build the application itself.
This will help you develop containers that can be easily re-used and deployed to production.
Changes From Development To Production
In order to move our website into production, we will have to serve the output of the Gatsby builds via NGINX. For this we’ll make two new files to be used in production:
- Dockerfile
- docker-compose.prod.yml
Our docker-compose file will remain mostly unchanged, most of the changes will be to the Dockerfile.
FROM node:alpine as builder
WORKDIR /appCOPY package.json .
RUN npm install
COPY . .
RUN ["npm", "run", "build"]FROM nginx EXPOSE 80
COPY --from=builder /app/public /usr/share/nginx/html
Builder Phase
In Docker, you have specified different phases to have different base images. These stages are specified with the ‘FROM’ keyword.
This allows us to easily build our website and then copy our files over to the NGINX server.
Serve Files Via NGINX
Gatsby will build the output over to the public folder in the working directory. Hence, we can just copy this over to the default directory that NGINX checks to server static files.
You can review more about the default directories (yes there are multiple) here.
We utilize NGINX since it an incredibly fast web server that’s great for request routing and serving static files. Its also incredibly configurable, allowing you to use compression algorithms to even further optimize performance.
docker-compose.prod.yml
version: "3"
services:
client:
build:
context: ./client
dockerfile: Dockerfile
ports:
- "80:80"
Docker Compose In Production
There are very few changes to our compose files, only changing the Dockerfile and port mapping.
Though for more complex applications, your production configuration may require more complex environmental variables and logging changes.
There are more configurations that you can keep in mind when using Docker Compose in production, which can be found in the official docs.
Why Change The Port Mapping
The port mapping change is required since port 80 is the default port that will allow the website to be accessed without having to specify the port.
Running Production
Once you’re done working on your Gatsby website, and finished with development, you run the production version of your website via:
docker-compose -f docker-compose.prod.yml up
And make sure to add the build tag if you have updated the underlying Dockerfile.
docker-compose -f docker-compose.prod.yml up --build
Conclusion
Congratulations! Now you should have a smooth development environment for your Gatsby applications and a production configuration ready for deployment.
Folder Structure
The following should be the general structure that you should have in the end.
client/
...
Dockerfile.dev
Dockerfile
docker-compose.yml
docker-compose.prod.yml
Further Steps
If you need more performance or have more complex applications you should look into other services and configurations that you can include to serve your needs.
Here are some ways that you could expand on this setup:
- Create an NGINX Reverse Proxy (separate from the one serving the Gatsby site) to send the requests to either the Gatsby client or an API
- Optimize the Gatsby NGINX configuration to use more robust compression algorithms (Gzip or Brotli compression) and other performance optimizations.
- Create a file watcher that will automatically update a running container’s npm modules (for development