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.
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!!!
But was it actually working in dev? Was the frontend fetching data from the backend at build time?
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.
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.
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.
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
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.
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.