In the last dev containers post, I mentioned that one of the painful things I have to deal with is maintaining Gatsby sites. The real pain comes from at least one of them using a template that has a dependency on node-gyp
somewhere in its dependency chain. I don’t have good luck with that on Windows, so I was excited to learn about dev containers and containerize the Gatsby sites.
Gatsby Issues
In general, I get a lot of dependency issues whenever I have to upgrade the packages of the Gatsby static site builders. I suspect this is related to the templates that were chosen at the beginning and how the sites have evolved over time. Dependencies seem to depend on other ones in the chain and versions tend to clash:
Add to it that while Gatsby on Windows is documented, it mentions Visual Studio 2015 and Visual Studio 2017. I would rather not install older tools. Every time I have to install the older build tools for other platforms, it runs into other issues for other projects. It’s a mess! Also, in case you haven’t looked at the Gatsby on Windows documentation, know that they call out issues with node-gyp
- so it’s not just me!
Every time I look at the repos for the Gatsby sites I have to maintain, I would feel very unhappy because my laptop didn’t have Node.js installed, and I prefer to keep it that way. I didn’t want yet another language that I’d have to maintain multiple versions nor did I want to have to deal with dependency chaos on top of dependency issues on Windows.
Call in the Dev Containers!
I finally got tired of dealing with the pains of these sites and realized I could remove some of the friction by taking Windows out of the development environment. How did I do this? Linux-based dev containers!
For this setup I have a Dockerfile
and a devcontainer.json
. I want to share those with you.
Dockerfile
This is the Dockerfile
I use, which includes installing the basic development tools, setting up sudo
access for our node
user, installing the Gatsby command-line tools, and setting up an environment variable for dev containers:
FROM node:18.15.0
# Install basic development tools
RUN apt update && apt install -y less man-db sudo
# Ensure default `node` user has access to `sudo`
ARG USERNAME=node
RUN echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME
# Install the Gatsby CLI
RUN npm install -g gatsby-cli --unsafe-perm=true --allow-root
# Set `DEVCONTAINER` environment variable to help with orientation
ENV DEVCONTAINER=true
# Use this to enable polling when Docker is used
ENV CHOKIDAR_USEPOLLING=true
# This is also used for Hot Module Reloading with Gatsby
ENV INTERNAL_STATUS_PORT=5001
If you’re wondering why this particular version of Node, it’s because that particular site is running Gatsby v4. It’s been a huge undertaking to attempt to move it to Gatsby v5. For now, it’s running an older version. I’m okay with this, as it isn’t polluting my local machine with multiple versions of Node.
devcontainer.json
This is what my devcontainer.json
contains:
{
"name": "SITE_NAME Gatsby blog",
"build": {
"dockerfile": "Dockerfile"
},
"appPort": [8000,9000],
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
"workspaceFolder": "/workspace",
"remoteUser": "node",
"mounts": [
"source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
],
"runArgs": ["--name","SITE_NAME_gatsby_devcontainer"],
"postCreateCommand": "sudo chown node ${containerWorkspaceFolder}/node_modules && sudo npm install --legacy-peer-deps && sudo npx playwright install-deps",
"postStartCommand": "npx playwright install && gatsby clean && gatsby develop --host 0.0.0.0"
}
I reference the Dockerfile
as part of build
. For appPort
, I publish ports 8000 and 9000 so that we can expose the gatsby develop
and gatsby serve
commands, respectively. Since I am working with Node and node_modules
, I’m going to use a named volume for storing the node_modules
folder. This is where workspaceMount
,workspaceFolder
, and mounts
come into play. The mounts
entry is the way to create mounts to a container and works cross-orchestrator. The type
in the mounts
string specifies the type of mount. We are using volume
instead of bind
based on the Visual Studio Code - Advanced Containers - Improve performance guidance. The workspaceMount
sets the default local mount point for the workspace when the container is created, and workspaceFolder
goes with this as it creates the folder that is the default path when connecting to the container.
The runArgs
are arguments that get passed in via the Docker CLI. This is an array of parameters as strings. Since I have multiple Gatsby sites, I update the runArgs
in my devcontainer.json
for each Gatsby site to have a unique name.
postCreateCommand
vs postStartCommand
?
I have commands for both postCreateCommand
and postStartCommand
. Let’s talk about why I have both and how they differ.
The postCreateCommand
is the last of the commands that are executed when a dev container is created. It happens after the dev container has been assigned to a user for the first time. In my postCreateCommand, I am changing the owner of the node_modules
folder and installing Playwright dependencies. For this particular Gatsby site, we use Playwright as part of the process of generating Mermaid diagrams. So this is a timing issue.
The postStartCommand
is executed every time the container is started successfully. When I start my dev container, Playwright gets installed, Gatsby clean
runs, and then Gatsby build
runs. I tried moving the Playwright installation to the postCreateCommand
and ran into issues, so that’s why it appears in postStartCommand
. When I start the container, I want to know the state of my Gatsby site; this is why I clean and build the site on start.
Hey, What’s that —host?
You may notice that I run gatsby develop --host 0.0.0.0
as part of my postStartCommand
. What’s going on there?
Publishing the ports lets them go out over localhost
(127.0.0.1
). However, this local
part applies to “this machine” or “this container”. In this case, it applies only to traffic within the container - not outside the container.
We need to take one more step so that outside the container can connect to the site running in the container. To do this, we will connect to the container via its “all interfaces” IP of 0.0.0.0
.
The --host 0.0.0.0
isn’t unique to Gatsby either - this applies to Flask, Nest.js, and many others.
Conclusion
By adopting Linux-based dev containers, I’ve eliminated the pain points of maintaining Gatsby blogs on Windows. Dockerfile and devcontainer.json files allowed me to fine-tune my development environment, and the use of mount points and volume mounts for node_modules ensured smooth performance without any unwanted surprises. The flexibility of postCreateCommand and postStartCommand gave me control over my workflow, streamlining the process even further. In the end, I gained a stable, predictable development setup that works exactly how I need it to - without Windows getting in the way.