Gatsby Docker Env Vars Alternative

Getting GatsbyJS To Work With Environment Variables At Docker Run Time

Getting GatsbyJS To Work With Environment Variables At Docker Run Time

The Problem

Does Gatsby support environment variables? Yes, but only at build time.

So technically you’ll get this page with the current convention of environment variables passed in a Docker container with Gatsby

TL;DR

Skip to “Project Repository” section.

TypeScript Users

There are a few more steps for this process that I’m going to add in another tutorial with the full setup of Gatsby, TypeScript, and this Docker implementation (I̶’̶l̶l̶ ̶u̶p̶d̶a̶t̶e̶ ̶t̶h̶i̶s̶ ̶s̶e̶c̶t̶i̶o̶n̶ ̶w̶i̶t̶h̶ ̶a̶ ̶l̶i̶n̶k̶ w̶h̶e̶n̶ ̶i̶t̶’̶s̶ ̶o̶u̶t̶). Here’s the article:

The Scenario

We want to create an application with Gatsby where we will pass it an environment variables to a backend API to fetch certain data. In this case, we’re going to use https://jsonplaceholder.typicode.com.

Why Would You Do This? Why Not Just Hard Code It?

If are running multiple environments (develop, staging, production) for your development process, then you will likely have different backend environments set up that connects to different data. If you hard code some of those values, then you would have to package each of those hard coded values. It makes sense to create one Docker image that is able to handle different environment variables to ping different servers for a different set of testing or production data.

Passing different values for different environments

Why Not Just Use Gatsby’s Built-In Environment Variable Support

Sure, you could use the recommend .env, but the issue comes when the site is being deployed. You would need to pass it the environment variables, and then get the Docker image to run the Gatsby build script to include those environment variables. That would then make your application go down until it has done being built, which isn’t ideal for continuous uptime.

Setting Up New Project

I’m going to examine the recommend method from Gatsby, demo how it works with Docker, and then show how to overcome the down time.

gatsby new gatsby-env-vars;
cd gatsby-env-vars;
yarn develop;
New Gatsby Project

If we look at the official documentation, Gatsby mentions that you can create environment variables with the standard dotenv library built in.

Creating Our Environment Variable File

echo 'GATSBY_API_URL=https://jsonplaceholder.typicode.com' > .env

Adding support to read from the new environment variables file.

File: gatsby-config.js

require("dotenv").config({
path: `.env`,
});
// ...rest of code

Creating Out Fetch Request

File: src/pages/index.js

import * as React from "react"import Layout from "../components/layout"
import SEO from "../components/seo"
const IndexPage = () => {
const [todos, setTodos] = React.useState()
React.useEffect(() => {
fetch(`${process.env.GATSBY_API_URL}/todos`)
.then(response => response.json())
.then(json => setTodos(json))
}, [])
return (
<Layout>
<SEO title="Home" />
<h1>Todos</h1>
{todos && todos.length > 0 && (
<ul>
{todos.map((todo, key) => (
<li key={`todo-${key}`}>{todo?.title}</li>
))}
</ul>
)}

</Layout>
)
}
export default IndexPage
Gatsby Todos Being Fetched From API

Creating Our First Docker Implementation

The plan is to pass an environment variable to Docker at run time which will point to our API url, and use it with our Gatsby application.

First our docker configuration files:

File: ./Dockerfile

# BASE IMAGE
FROM node:12.18.4-alpine
# ADDITIONAL LIBRARIES NEEDED FOR BUILD
RUN apk update; \
apk add libpng-dev; \
apk add autoconf; \
apk add automake; \
apk add make; \
apk add g++; \
apk add libtool; \
apk add nasm; \
apk add nginx; \
mkdir /run/nginx/; \
mkdir /usr/share/nginx; \
mkdir /usr/share/nginx/html;
WORKDIR /usr/share/nginx/htmlCOPY . /usr/share/nginx/html# OUR CONFIGURATION FILES FOR ENVIRONMENT VARIABLES
COPY $PWD/docker/entrypoint.sh /usr/local/bin
# NGINX CONFIGURATION TO MAKE OUR PUBLIC FOLDER ACCESSIBLE
COPY $PWD/docker/default.conf /etc/nginx/conf.d/default.conf
RUN chmod +x /usr/local/bin/entrypoint.shENTRYPOINT ["/bin/sh", "/usr/local/bin/entrypoint.sh"]RUN yarn install --non-interactive --frozen-lockfileCMD ["/bin/sh", "-c", "exec nginx -g 'daemon off;';"]

Creating our entry point startup bash script for Docker.

File: docker/entrypoint.sh

# SET DEFAULT ENV IF NOT SET
export GATSBY_API_URL="${GATSBY_API_URL:=unknown}";
# CHECK IF FILES IS NOT CREATED, ELSE CREATE
cd /usr/share/nginx/html;
if [ ! -f .env ];
then
echo "GATSBY_API_URL=$GATSBY_API_URL" > .env
fi;
# BUILD PROJECT
yarn build;
# KEEP NGINX DAEMON RUNNING
nginx -g 'daemon off;'; nginx -s reload;

Creating our Nginx configuration file default.conf

File: docker/default.conf

server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
root /usr/share/nginx/html/public;
index index.html index.htm;
}
# You may need this to prevent return 404 recursion.
location = /404.html {
internal;
}
}

Now if we build the project and run it, you’ll see the issue we get.

docker build . -t gatsby-building;
docker run -it -d -p 8000:80 -e GATSBY_API_URL=https://jsonplaceholder.typicode.com --name gatsby gatsby-building;

Now if you go to http://localhost:8000 you’ll see the following immediately:

This is because the Gatsby project is still building.

Gatsby Docker Image Still Building On Run — docker logs gatsby

If we wait a minute or two, and refresh the http://localhost:8000 you’ll see that the page has finally finished.

Gatsby Docker Build Finally Finishes

As you can imagine, deploying this through a deployment pipeline, where it does a health check, it will always return errors. Not to mention that it might show that “This page isn’t working” for a user trying to access the site for a small amount of time.

The Second Docker Implementation (The Fix)

What we need to do, because the code we’re running is on the frontend, is to have the build already be finished and then insert the environment variables, so that they could easily be swappable.

Remove Environment Variable Support

In order for this to work, we need to remove what we did before, because this method won’t work for what we need to do.

rm .env

File: gatsby-config.js

- require("dotenv").config({
- path: `.env`,
- })
// Rest of code

Add Static Folder Support

We’re going to leverage the static folder support from Gatsby to add our own JavaScript script that contains our environment variables.

mkdir static;
echo 'window.GATSBY_API_URL="https://jsonplaceholder.typicode.com"' > static/env.js;

We also need to ignore this file to make sure it isn’t added to our repository.

File: .gitignore

# dotenv environment variable files
.env*
static/env.js

Adding The File In The HTML Head

In order for us to take advantage of this new file, we need to make sure it’s added in the html <head> of our entire project.

To do this, we’re going to leverage Gatsby’s html.js file for customizing our html pages that are generated.

In order for this to work, you’ll need to make sure that you have a .cache folder present in your repo. If it’s not there, just run a quick yarn build to generate it. Then run this command:

cp .cache/default-html.js src/html.js;

In our newly copied file we’re going to add our env.js file in the html <head>:

File: src/html.js

import React from "react"
import PropTypes from "prop-types"
export default function HTML(props) {
return (
<html {...props.htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<script src="/env.js"></script>
{props.headComponents}
</head>
<!--- REST OF CODE --->

Now if we build our page, we can see that our env.js is being added.

yarn develop;
env.js being passed to our Gatsby project

You’ll also noticed that our project isn’t working anymore.

Handling The New Environment File

If we go ahead and change our original file and replace it the correct environment variable name we should see the following:

File: src/pages/index.js

import * as React from "react"import Layout from "../components/layout"
import SEO from "../components/seo"
const IndexPage = () => {
const [todos, setTodos] = React.useState()
React.useEffect(() => {
fetch(`${window.GATSBY_API_URL}/todos`)
.then(response => response.json())
.then(json => setTodos(json))
}, [])
// Rest of code
Working with new env.js file

Great, it’s working, so now we just need to wrap up our revised Dockerfile and entrypoint file. (If you’re a TypeScript user, our job isn’t done yet, you’ll need to follow these next steps).

Docker Configuration

The three files we’ll need to modify are our default.conf, entrypoint.sh, our Dockerfile, and .dockerignore.

We no longer need our nginx configuration file because we’re going to use the native folder setup by the docker image nginx:1.15.4-alpine.

rm docker/default.conf

Next we’re going to modify our entrypoint.sh file to create the env.js file with the environment variable passed.

File: docker/entrypoint.sh

# SET DEFAULT ENV IF NOT SET
export GATSBY_API_URL="${GATSBY_API_URL:=unknown}";
# CHECK IF FILES IS NOT CREATED, ELSE CREATE
cd /usr/share/nginx/html;
if [ ! -f env.js ];
then
echo "window.GATSBY_API_URL='$GATSBY_API_URL';" > env.js
fi;
#̶ ̶B̶U̶I̶L̶D̶ ̶P̶R̶O̶J̶E̶C̶T̶
y̶a̶r̶n̶ ̶b̶u̶i̶l̶d̶;̶
# KEEP NGINX DAEMON RUNNING
nginx -g 'daemon off;'; nginx -s reload;

Update July 9, 2021:

Credit to Ezequiel Gonzalez Rial for noticing that yarn build should not be in this file and will give an error when the image is run. This is because below in the Dockerfile I separate the process in two steps, build, and serve built, which doesn’t contain and won’t recognize yarn build in the entrypoint.sh in the second step.

/usr/local/bin/entrypoint.sh: line 13: yarn: not found

Article continues now.

We’re also going to modify our .dockerignore file to ignore our new env.js file.

File: .dockerignore

node_modules/
*/node_modules/
.git
public
.cache
package-lock.json
- .env
+ static/env.js

Lastly, we have our Dockerfile, which we’re going to separate into two steps, a build step and the final location for the deployment.

File: Dockerfile

# BUILD PROCESS
FROM node:12.18.4-alpine as build-stage
RUN apk update; \
apk add libpng-dev; \
apk add autoconf; \
apk add automake; \
apk add make; \
apk add g++; \
apk add libtool; \
apk add nasm;
WORKDIR /usr/src/appCOPY package.json yarn.lock /usr/src/app/RUN yarn install --non-interactive --frozen-lockfileCOPY . ./RUN yarn run build --verbose# BUILT APP
FROM nginx:1.15.4-alpine
WORKDIR /usr/share/nginx/htmlCOPY --from=build-stage /usr/src/app/public /usr/share/nginx/htmlCOPY $PWD/docker/entrypoint.sh /usr/local/binRUN chmod +x /usr/local/bin/entrypoint.shENTRYPOINT ["/bin/sh", "/usr/local/bin/entrypoint.sh"]EXPOSE 80CMD ["/bin/sh", "-c", "exec nginx -g 'daemon off;';"]

Now if we run a docker build, you’ll see that it is setup into two steps with the final destination being the nginx container image.

docker build . -t gatsby-building
Docker build stage
Docker final placement of files

Now if we run the docker image, we can see that everything is working with our new env.js file.

docker run -it -d -p 8000:80 -e GATSBY_API_URL=https://jsonplaceholder.typicode.com --name gatsby gatsby-building;
Working with our new env.js

This means no waiting for the build process when we want to deploy the image, and still being able to pass environment variables to it at run time.

Now if the environment variable for the GATSBY_API_URL needed to change, you can just do another docker run with a new value, and there would be no wait time for the build process.

Project Repository

If you’re interested in seeing the code, you can get all of it here.

What’s Next

The next step would be make a TypeScript version, and I also have an implementation to get this working with Auth0 as a more real world scenario for this kind of use.

If you got value from this, please share it on twitter 🐦 or other social media platforms. Thanks again for reading. 🙏
Please also follow me on twitter: @codingwithmanny and instagram at @codingwithmanny.

Other Articles I’ve Written

Web Application / Full Stack JavaScript Developer & Aspiring DevOps