Docker Hostname ENOTFOUND in Next.js Prod but Not in Dev
Why next dev reaches your Compose service while the production image build cannot resolve it, proved on Next.js 16.4.0, with a fix for the Pages Router and the App Router.
TL;DR#
Error: getaddrinfo ENOTFOUND <service> that appears only in the production image, while next dev reaches the same service, comes from where the data fetch runs. getStaticProps (or a static App Router page) runs during next build, and a RUN next build line in a Dockerfile executes in a build container that is not attached to the Compose network, so Docker's DNS has no entry for ghost-api. Move the fetch to request time: getServerSideProps in the Pages Router, or await connection() before the fetch in the App Router. Both were reproduced on Next.js 16.4.0 below.
The error from the build log#
The Stack Overflow question describes a Next.js front end composed with Ghost, MySQL and NGINX. It works under next dev; after next build and next start it logs Error: getaddrinfo ENOTFOUND ghost-api on server-side requests to the Ghost container. Rebuilt with the same shape on Next.js 16.4.0 and Docker Compose 5.5.0 (a page whose getStaticProps fetches http://ghost-api:2368/posts, served by a stub Node HTTP service named ghost-api that returns a JSON list of posts, running on the Compose network in place of the real Ghost image), docker compose build stops here:
#9 9.430 Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
#9 9.497 TypeError: fetch failed
#9 9.497 [cause]: Error: getaddrinfo ENOTFOUND ghost-api
#9 9.497 syscall: 'getaddrinfo',
#9 9.497 hostname: 'ghost-api'
#9 9.497 Export encountered an error on /, exiting the build.The #9 prefix is BuildKit's step number: the failure happens inside RUN npx next build, not at next start. Code that catches the rejection changes how it looks, not where it comes from: the asker's getPosts helper turns a failed Ghost request into an error prop, so the build can finish and the error string ships inside the prerendered page, to be seen only when the page is served.
Why it happens#
Two facts, each documented, meet in this error.
getStaticProps runs at build time. The Next.js docs state that "getStaticProps always runs during next build", and, in a separate section, that "In development (next dev), getStaticProps will be called on every request" (getStaticProps, Next.js docs). So under next dev the fetch happens inside the running container, when a browser hits the page. In production it happens once, wherever next build runs.
A Dockerfile build is not on the Compose network. In the asker's repository the frontend Dockerfile runs RUN yarn build and only then CMD ["yarn", "start"]. Compose service names resolve through the DNS of the project network, and only containers attached to that network can use it. The build step is not one of them, and Compose cannot attach it to one: the Compose reference says the build network option "is not a Compose network from the top-level networks key", and "The value is a mode (default, none, or host), not a network name" (Compose build reference).
Put together: dev fetches at request time from a container that can resolve ghost-api; prod fetches at build time from a container that cannot. It is not that Node "makes requests differently after being built", as the question suspects.
The repository's own history agrees. Its index page used getStaticProps in mid-June 2020, and commit 4b9e7ea, "switch function", later replaced it with getServerSideProps.
Proving it is the location, not dev versus prod#
The decisive control is to run the same next build, with the same page, from a container that is on the Compose network. The control that was run had this shape, where <project>_default is the network Compose created for the stack and <image-with-deps> is any image holding the app and its installed dependencies:
docker run --rm --network <project>_default \
-e GHOST_API_URL=http://ghost-api:2368 \
-w /app <image-with-deps> npx next buildIt succeeds and prerenders the page with the data from ghost-api:
✓ Generating static pages using 3 workers (3/3) in 88ms
┌ ● /
● (SSG) prerendered as static HTML (uses getStaticProps)Same Next.js, same production build, same hostname, and the only difference is the network the process sits on. This is also why the second answer on the question saw no error after changing the container command to run next build && next start: the build then ran inside the running service.
What does not cause it#
- The environment variable. The accepted answer suggests moving the URL into
serverRuntimeConfig. The log above showshostname: 'ghost-api': the URL was read, and the DNS lookup is what failed. A missing variable would not reach DNS at all: Node's fetch rejects the resulting URL undefined/posts with TypeError: Failed to parse URL from undefined/posts (checked on node:22-alpine), so no host is ever looked up. On current Next.js the option is gone anyway: "serverRuntimeConfigandpublicRuntimeConfighave been removed. Use environment variables instead." (Next.js 16 upgrade guide). - A browser-side request.
getaddrinfois Node's resolver. A request made by the browser never goes through it, so this exact message always comes from server-side code: a data function, a server component, a route handler, or the build.
Fix#
Fetch from the service when a request arrives, not when the image is built.
- Pages Router: switch
getStaticPropstogetServerSideProps. The getServerSideProps docs describe it as a function to "fetch data and render the contents of a page at request time". The body of the function stays identical:
export default function Home({ posts }) {
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
export async function getServerSideProps() {
const res = await fetch(`${process.env.GHOST_API_URL}/posts`);
const { posts } = await res.json();
return { props: { posts } };
}- App Router: stop prerendering before the fetch. A server component with a plain
fetchis prerendered bynext buildtoo, and fails with the samegetaddrinfo ENOTFOUND ghost-apiin the build log. The connection() reference says the function lets you "indicate rendering should wait for an incoming user request before continuing":
import { connection } from 'next/server';
export default async function Page() {
await connection();
const res = await fetch(`${process.env.GHOST_API_URL}/posts`);
const { posts } = await res.json();
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}- Give the running container the service URL. Steps 1 and 2 are the fix; this step only keeps a Compose hostname out of the image. The reproduction itself set the URL with an
ENV GHOST_API_URL=http://ghost-api:2368line in the Dockerfile, so the variable existed at build time and at run time, and the build still passed once the fetch moved to request time. Declaring it in the service'senvironment:block instead means the build never sees a host it cannot resolve. A Compose file of this shape does that;ghost-apiis a stub (a small Node server answering/postswith JSON on port 2368), not Ghost itself. With the real Ghost image the fetch has to target its Content API path with a key, or go through the@tryghost/content-apiclient as the asker's repository does; the network fix is the same either way:
services:
ghost-api:
image: node:22-bookworm-slim
command: ["node", "/srv/server.js"]
volumes:
- ./api:/srv
web:
build: ./web
environment:
GHOST_API_URL: http://ghost-api:2368
depends_on: [ghost-api]If the page genuinely must be static, the data has to be reachable from the build: an address the build container can resolve, or data fetched before docker build and copied into the context. A Compose service name is neither.
Verify the fix#
Rebuild the image and check the route table. With getServerSideProps the reproduction's build passes and marks the page dynamic:
#9 4.083 ┌ ƒ /
#9 4.084 ƒ (Dynamic) server-rendered on demandThen start the stack and request the page from inside the web container:
docker compose up -d web
docker compose exec -T web node -e "fetch('http://localhost:3000/').then(r=>r.text()).then(t=>console.log('GET / ->', t.match(/<ul>.*?<\/ul>/)[0]))"GET / -> <ul><li>Hello from ghost-api</li></ul>The App Router version with await connection() builds with the same ƒ / line and returns the same HTML. Reproduced on 11 October 2026 with Next.js 16.4.0 on node:22-bookworm-slim, Docker 29.7.2 and Compose 5.5.0.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
When the value comes from .env.production#
In commit 3d12442, the asker's repository moved the Ghost URL out of the Compose environment: block and into a committed frontend/.env.production file set to http://ghost-api:2368, and its Dockerfile copies the whole folder before building. Next.js sets NODE_ENV to production "for all other commands" than next dev and loads .env.$(NODE_ENV) (environment variables guide), so a next build inside docker build picks up the Compose hostname from that file even though the build cannot resolve it. That is why "the variable is set" and "the build cannot reach the host" are both true at once. The fix above holds whichever way the URL arrives, because the request-time code reads the variable inside the running container.
If the error changes after the fix#
A common workaround is to swap the service name for localhost. Inside a container, localhost is that container itself, so the error turns into connect ECONNREFUSED 127.0.0.1:<port>; the ECONNREFUSED in Docker Compose walkthrough explains why the service name is the right host. The same mistake shows up in CI, where a job running inside a container: must use the service label rather than localhost (ECONNREFUSED in GitHub Actions). The neighbouring App Router build error, where Next.js tries to prerender a route that reads request-time data such as cookies or headers, is covered in Dynamic server usage: couldn't be rendered statically. For the rest of the Compose setup (bind mounts, a named volume for node_modules), see the Compose development environment tutorial. With a Strapi back end the same swap produces fetch failed with ECONNREFUSED 127.0.0.1:1337, and the Next.js and Strapi ECONNREFUSED 1337 walkthrough explains why server code needs the Compose service name while anything the browser loads keeps the host address.
The short version for a reader with this error in front of them: if the hostname is a Compose service name and the stack trace mentions prerendering, the code is running in docker build, and the fix is to move that fetch to request time rather than to change the URL or the config file.
Further reading: Next.js builds locally, fails in Docker: fix the image error.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Next.js + Strapi in Docker: fetch failed ECONNREFUSED 1337
Read the address in the error, swap localhost for the Strapi service name in server-side fetches, and keep a separate public URL for anything the browser loads.
Next.js builds locally, fails in Docker: fix the image error
The exact same source code compiles on your laptop and crashes in the container because the container runs a Node major version your local shell does not — add sharp and the crash disappears.
Fix Next.js Docker: Cannot find module '/app/server.js'
Follow the server entrypoint from the Next.js build into the final image, then check the application and its static files separately.


