Running Gatsby in a Dev Container

September 23, 2024#Tools
Article
Author image.

Sarah Dutkiewicz, Senior Trainer

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:

Spiderman pointing at Spiderman pointing at Spiderman - all dependencies nice and dependent on each other

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.


Copyright © 2024 NimblePros - All Rights Reserved