The Problem
Does Gatsby support environment variables? Yes, but only at build time.
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.
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;
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
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.confRUN 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.
If we wait a minute or two, and refresh the http://localhost:8000
you’ll see that the page has finally finished.
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;
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
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-stageRUN 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-alpineWORKDIR /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
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;
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.