A gotcha with Next.js production builds in Docker Compose
Tags: docker, nextjs, beginners, webdev
Published: 2023-04-26 23:20:01

This post discusses a gotcha or pitfall I ran into when learning Next.js and Docker. It's pretty elementary, but I couldn't find discussions of it online. ChatGPT wasn't especially helpful either, so hopefully this post saves another newb some time and frustration. 

TL;DR: you can put `npm run build` in a command to run when the container starts, not when the image builds. 

## The problem: Docker builds succeed in dev but not prod

I had a Docker Compose file defining backend and frontend services, the latter a Next.js app. I was able to build the images and start the containers fine in a development environment. But in production it failed every single time!

The error was typically some version of:
```
FetchError: request to http://backend:4000/users failed
reason: getaddrinfo ENOTFOUND backend
```

In plain English, the frontend was unable to fetch data from the backend when Next.js went to build the production version of the app.

But it worked fine in dev!!!

Right?

Riiiiight?...

But was it actually working in dev? Was the frontend fetching data from the backend at build time? 

Hint: no.

## SSG and `npm run build`

Next.js offers some great features like static-site generation (SSG). Static-site generation means that the server pre-fetches data so that it can use that data to generate completed HTML and then serve that HTML to web browsers in production. Now _when_ is that data fetched? Answer: on running `npm run build` (or `next build`) _prior_ to starting to the frontend server.

So building a production-ready SSG-enabled Next.js app requires access to backend data. Which means that the server supplying that data needs to be up and running first. 

By contrast, in development, no static HTML is generated so there is no build step requiring data to be pre-fetched.

In essence, then, my error boils down to including `RUN npm run build` in my production frontend Dockerfile and then using Docker compose to build both services before starting either. 

I was trying to tell Next.js to build a static site before the backend server was up and running to supply the necessary data.

## How to fix it?

That explains why I was running into the error. But how to resolve it?

First, the problem is muddied by semantic confusion over the term `build`. Docker builds images and Next.js builds a statically generated site. It's natural at first to think these are similar operations that need to go together. 

But there's no necessary connection between them. Building the Docker image is a matter of packaging an app together with its dependencies and runtime environment. Building a statically generated site is a matter of fetching data and compiling React and JavaScript into optimized HTML.

If the data is there to be fetched, the latter build can happen as part of the former. But, as in my case, if the data is not there, the Next.js build has to wait.

So the problem can be rephrased as: how to build the backend and frontend Docker images first, then start the backend server, then do the Next.js build, then start the frontend server. 

## Docker entrypoints

Can't we just use the `depends_on` directive in the Docker compose file? `depends_on` says "don't start A without first starting B." And, yes, we want to make sure the backend service starts first. But there's a bit more to it. 

We also need to make sure that the Next.js build happens after the backend starts. `depends_on` establishes a start dependency, not a build dependency. We could solve our problem if there were a Docker compose directive to say "don't build A without first starting B", but as far as I know there isn't. 

What we can do is separate building the frontend Docker image from building the statically generated site. That way `depends_on` will ensure that the Next.js build happens at the right time. 

The easiest way I found to do this is with a simple docker-entrypoint.sh file. Entrypoints are scripts for specifying commands to run when a container starts up. Exactly what we need! The hard part was just realizing the Next.js build could happen on container start not on image build. 

My docker-entrypoint.sh file looked like this:

```
#!/bin/bash

# Build the Next site including SSG
npm run build

# Start the production server
npm run start
```
Pretty straightforward. 

And the final step was just invoking the entrypoint as the final lines in my frontend dockerfile, replacing the `CMD ['npm', 'run', 'dev']` line that was there previously:

```
COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/bin/sh", "/docker-entrypoint.sh"]
```
The first line above copies the script into the Docker image, the second makes it executable, and the third executes it. 

## Final thought

Learning new technologies often means learning different uses of the same word, 'build' for example. Don't assume they mean the same thing! Be sensitive to what the words are doing in context--the concepts are likely different, maybe radically, maybe only very slightly. Once we clarify the underlying the concepts, the code usually follows.