DEV Community

mattIshida
mattIshida

Posted on

A gotcha with Next.js production builds in Docker Compose

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (6)

Collapse
 
vsaw profile image
Valentin Sawadski (he/him)

Hm, to me this sounds like you have to check how you get your props. NextJS has different hooks to populate page data at build or runtime (see nextjs.org/docs/basic-features/dat...) and you should check that any data really is available at build time, otherwise you should use nextjs.org/docs/basic-features/dat... instead.

Collapse
 
tbroyer profile image
Thomas Broyer

One small nit: always use exec for the last command in your entrypoint script (exec npm run build) such that the npm process will receive signals (such as SIGTERM to stop the container) rather than the script. Without it, the shell will receive the signals and ignore them (that's how it works when it's PID 1), do after a timeout the Docker daemon will force-kill (SIGKILL) it.

Now if you fetch data from an external data source, how about using incremental static generation? That way, IIUC, you could build the site upfront without the need for that data (but then pay the cost of generation on first access to a page πŸ€·β€β™‚οΈ)

Collapse
 
fadyamir223 profile image
Fady Amir • Edited

I guess to laverage multi stage build we can build the next app first with Dockerfile like dev but using .env.production* and the production database then copy .next/standalone and ./next/static files to the main image so we don't have to cache every first page visit.
But I don't know if it is applicable in CD pipeline

Collapse
 
farzadofficial profile image
Farzad

Hi,

I’m experiencing the exact same issue! Thanks for the solution but I haven’t yet managed to get his solution working!

Do you mind sharing the entire logic in a repo somewhere please?

Many thanks

Collapse
 
thomasbromehead profile image
thomasbromehead

Does that also mean that multi stage builds are useless when working on an SSG nextJS front?

Collapse
 
thomasbromehead profile image
thomasbromehead

Hey, thanks for this, super valuable.
Why did it work in development though...?
Do you mind sharing your Dockerfile and docker-compose.yml ?