All articles

Next.js 16 image optimizer blocks local IPs: how to allow them safely

Next.js 16 refuses to optimise images from hosts that resolve to a private IP address, so local setups that serve images from 127.0.0.1 or a *.lndo.site domain stop working. The images.dangerouslyAllowLocalIP option lifts the block. Set it from the image host itself, and not from NODE_ENV or a CI variable, so it can only be true where the image host really is local.

What does the error look like?

After upgrading to Next.js 16, every image served through next/image from a local backend failed. The optimizer fetches the source image on the server, and it now rejects the request when the hostname resolves to a loopback or private address. The guard exists to stop the image endpoint being used to reach internal services (a server-side request forgery risk), so the default is the right one for anything deployed.

Two common setups hit it straight away:

  • A Cypress job in CI that points the image host at 127.0.0.1 so tests run against a local stack.
  • Local development with Lando, which serves sites on *.lndo.site names that resolve to localhost.

Why isn't a NODE_ENV or CI check enough?

The first fix is the obvious one: turn the flag on when the environment looks non-production.

// next.config.js
module.exports = {
  images: {
    dangerouslyAllowLocalIP:
      process.env.CI === "true" || process.env.NODE_ENV === "development",
  },
};

This covered CI and next dev, and it left deployed builds with the guard on. It also missed a case. A common pre-push step is a production build run against the local stack, to catch problems before CI does. That build has NODE_ENV=production and no CI variable, but its image host still resolves to localhost. Every image was refused, and the failure looked like a broken site, not a config problem.

The underlying mistake is asking the wrong question. "Is this a development environment?" is a proxy. What the flag actually needs to know is "is the image host local?", and that is something the config can check directly.

How do you key the flag on the image host?

Most setups already pass the image host to Next.js through the allowed domains or remotePatterns setting, usually from an environment variable. Derive the flag from the same value:

// next.config.js
const imageHost = process.env.IMAGE_HOST ?? "";

const isLocalHost =
  /^(127\.0\.0\.1|localhost|.*\.lndo\.site)(:\d+)?$/.test(imageHost);

module.exports = {
  images: {
    domains: [imageHost],
    // Only relax the private-IP guard when the image host is itself local.
    dangerouslyAllowLocalIP: isLocalHost,
  },
};

Now the flag follows the data. A local production build with a Lando host gets it, so does the Cypress step on 127.0.0.1, and a staging or production host with a public name does not. There is no environment you have to remember to set or unset.

What are the limits of this approach?

  • The pattern is an allow-list of names, not a DNS check. It does not prove a name resolves locally. That is acceptable here because it errs towards keeping the guard on.
  • Keep the pattern tight. Anchor it with ^ and $. A loose match such as "contains localhost" could be satisfied by a public hostname that merely includes the string.
  • The same file may be copied across apps. If several Next.js apps share this setting, change them all together, or one of them will keep the old behaviour and fail only in its own pipeline.
  • Read the option name. It starts with "dangerously" for a reason. It deserves a comment that says why it is conditional.

How do you test it?

Because the flag is a pure function of one string, you can test the decision without starting Next.js. Pull the check into a small function and assert it:

const isLocalImageHost = (host) =>
  /^(127\.0\.0\.1|localhost|.*\.lndo\.site)(:\d+)?$/.test(host ?? "");

console.assert(isLocalImageHost("127.0.0.1:8080") === true);
console.assert(isLocalImageHost("site.lndo.site") === true);
console.assert(isLocalImageHost("images.example.com") === false);
console.assert(isLocalImageHost("localhost.example.com") === false);
console.assert(isLocalImageHost(undefined) === false);

The last two assertions matter most. They are the ones that would quietly turn the guard off somewhere it should be on.

Key takeaways

  • Next.js 16 blocks the image optimizer from fetching hosts that resolve to private IPs, which breaks loopback and Lando-style local image hosts.
  • dangerouslyAllowLocalIP lifts the block, but enabling it unconditionally removes a protection against server-side request forgery.
  • Environment proxies such as NODE_ENV and CI miss real cases, like a production build run locally before pushing.
  • Deriving the flag from the image host itself makes it true only when the host is local, with no per-environment settings to maintain.
  • Anchor the host pattern and test the negative cases, because a loose match is the way this change could silently reopen the guard.