Next.js Discord

Discord Forum

Next.js Image Optimization returns original JPEG in production (self-hosted, standalone, S3)

Unanswered
American Chinchilla posted this in #help-forum
Open in Discord
American ChinchillaOP
Hi everyone πŸ‘‹

I’m running a self-hosted Next.js app (v15.5.13) with output: "standalone" on Docker (Dokploy + Traefik), and I’m having an issue where Image Optimization does not work in production, even though it works locally.

---

⚠️ Problem

In production, requests to /_next/image return:
- content-type: image/jpeg
- content-length: ~6.7MB (same as original)
- even on x-nextjs-cache: MISS

Expected behavior:
- should return optimized image (WebP ~130KB)

---

βš™οΈ Setup

- Next.js (15.5.13)
- Deployment: Dokploy (Docker + Traefik)
- Image source: AWS S3
- Node: 24
- sharp (0.33.5)

I have downgraded sharp, because it was not working:
- native sharp failed because the linux-x64 binary requires x64-v2 / SSE4.2-level CPU features
- wasm sharp failed because Wasm SIMD is unsupported in that environment

next.config.ts:
images: {
    // Allowed remote images
    qualities: [1, 50, 60, 75],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24 * 7, // 7 days
    formats: ["image/webp"],
    remotePatterns: [
      {
        protocol: "https",
        hostname: "HERE_IS_MY_AWS_S3_URL",
        port: "",
        pathname: "/**",
        search: ""
      }
    ]
  }


Docker (Simplified):
FROM node:24 AS base
...
ENV NEXT_SHARP_PATH=/app/node_modules/sharp


---

❓ Question

Why would Next.js image optimizer fail

Is this:
- a limitation of standalone mode?
- related to Node 24?
- a known issue with remote images?
- or a silent fallback in the optimizer?

---

Any help appreciated! πŸ™

At this point I’m considering replacing /_next/image with a custom sharp API or pre-generated variants, but I’d really like to understand why the built-in optimizer fails here.

Thanks!

15 Replies

American ChinchillaOP
βœ… What works

Locally (same code, same S3 image):
- content-type: image/webp
- content-length: ~135KB

Manual test inside the container:
const sharp = require('sharp');
const res = await fetch('IMAGE_URL');
const buf = Buffer.from(await res.arrayBuffer());

const out = await sharp(buf)
  .resize({ width: 1920 })
  .webp({ quality: 60 })
  .toBuffer();

console.log(out.length); // ~135KB


So:
- sharp works
- image is valid
- conversion to WebP works

---

❌ What does NOT work

Production request:
/_next/image?url=...&w=1920&q=60

Response:
content-type: image/jpeg
content-length: 6714342
x-nextjs-cache: MISS

Second response:
content-type: image/jpeg
content-length: 6714342
x-nextjs-cache: HIT

Even after:
clearing .next/cache/images
rebuilding container
hard reload

---
@American Chinchilla Hi everyone πŸ‘‹ I’m running a self-hosted Next.js app (v15.5.13) with `output: "standalone"` on Docker (Dokploy + Traefik), and I’m having an issue where Image Optimization does not work in production, even though it works locally. --- ⚠️ Problem In production, requests to `/_next/image` return: - `content-type: image/jpeg` - `content-length: ~6.7MB` (same as original) - `even on x-nextjs-cache: MISS` Expected behavior: - should return optimized image (WebP ~130KB) --- βš™οΈ Setup - Next.js (15.5.13) - Deployment: Dokploy (Docker + Traefik) - Image source: AWS S3 - Node: 24 - sharp (0.33.5) I have downgraded sharp, because it was not working: - native sharp failed because the linux-x64 binary requires x64-v2 / SSE4.2-level CPU features - wasm sharp failed because Wasm SIMD is unsupported in that environment `next.config.ts`: images: { // Allowed remote images qualities: [1, 50, 60, 75], deviceSizes: [640, 750, 828, 1080, 1200, 1920], imageSizes: [32, 48, 64, 96, 128, 256, 384], minimumCacheTTL: 60 * 60 * 24 * 7, // 7 days formats: ["image/webp"], remotePatterns: [ { protocol: "https", hostname: "HERE_IS_MY_AWS_S3_URL", port: "", pathname: "/**", search: "" } ] } Docker (Simplified): FROM node:24 AS base ... ENV NEXT_SHARP_PATH=/app/node_modules/sharp --- ❓ Question Why would Next.js image optimizer fail Is this: - a limitation of standalone mode? - related to Node 24? - a known issue with remote images? - or a silent fallback in the optimizer? --- Any help appreciated! πŸ™ At this point I’m considering replacing /_next/image with a custom sharp API or pre-generated variants, but I’d really like to understand why the built-in optimizer fails here. Thanks!
Palomino
Diagnosing the /_next/image Issue πŸ”
Based on your setup, this is almost certainly a silent sharp fallback β€” Next.js can’t use sharp, so it falls back to passing the original image through unoptimized. Here’s the full breakdown:

Root Cause: No working sharp = no optimization
Next.js image optimization requires sharp at runtime in standalone mode. When sharp fails to load (for any reason), Next.js does not throw an error β€” it silently serves the original image as-is. That’s exactly what you’re seeing: same file size, correct content-type, but no actual optimization.
You’ve already identified both sharp failure modes:



|Mode |Your Error |
|-------------------|-----------------------------------------------------|
|sharp (native) |Requires SSE4.2 / x64-v2 CPU β€” your host CPU is older|
|@img/sharp-wasm32|Wasm SIMD unsupported in your Node/V8 environment |

Why NEXT_SHARP_PATH Isn’t Enough
Setting NEXT_SHARP_PATH tells Next.js where to find sharp, but if the binary at that path can’t actually execute (CPU mismatch, Wasm SIMD missing), the import fails silently and optimization is skipped. The env var doesn’t fix the underlying incompatibility.

Fix Options (in order of preference)
Option 1: Force a compatible sharp build (recommended)
The native SSE4.2 requirement comes from the prebuilt binary. You can force a build from source inside your Docker image, which compiles for your specific CPU:

FROM node:24 AS base

# Install build deps for compiling sharp from source
RUN apt-get update && apt-get install -y \
python3 make g++ libvips-dev \
&& rm -rf /var/lib/apt/lists/*

# Force sharp to compile from source instead of using prebuilt
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
ENV npm_config_sharp_binary_host=""

# After your npm/pnpm install:
RUN npm rebuild sharp --build-from-source
# or: pnpm rebuild sharp --build-from-source


This compiles sharp against the actual CPU in your Docker host rather than relying on a prebuilt binary that assumes SSE4.2.

Option 2: Pin to a sharp version with a broader prebuilt
sharp 0.32.x prebuilts had slightly broader CPU compatibility. Worth trying before going full compile-from-source:

npm install sharp@0.32.6


Option 3: Use a Wasm build with SIMD disabled
If Wasm SIMD is the blocker, you can try forcing the non-SIMD Wasm path:

ENV NEXT_SHARP_PATH=/app/node_modules/@img/sharp-wasm32


And install it explicitly:

npm install @img/sharp-wasm32


Note: this is significantly slower than native but will optimize images correctly.

Option 4: Verify sharp actually loads at startup
Add this to your next.config.ts temporarily to confirm whether sharp is loading:

// At the top of next.config.ts
try {
const sharp = require('sharp');
console.log('βœ… sharp loaded:', sharp.versions);
} catch (e) {
console.error('❌ sharp failed to load:', e.message);
}


Check your Docker container logs on startup β€” if you see the ❌, that confirms the silent fallback.

Option 5: Custom /api/image route (your fallback plan)
If none of the above work due to infrastructure constraints, a custom route using sharp directly gives you full control:

// app/api/image/route.ts
import sharp from 'sharp';

export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const src = searchParams.get('src')!;
const w = Number(searchParams.get('w') ?? 1200);
const q = Number(searchParams.get('q') ?? 75);

const res = await fetch(src);
const buffer = Buffer.from(await res.arrayBuffer());

const optimized = await sharp(buffer)
.resize(w)
.webp({ quality: q })
.toBuffer();

return new Response(optimized, {
headers: {
'Content-Type': 'image/webp',
'Cache-Control': 'public, max-age=604800, immutable',
},
});
}
Quick Diagnosis Checklist βœ…

[ ] Check container logs on startup for sharp load errors
[ ] Run: docker exec <container> node -e "require('sharp')"
[ ] Check CPU: docker exec <container> grep flags /proc/cpuinfo | grep sse4_2
[ ] Confirm Node version inside container: docker exec <container> node --version


The SSE4.2 check is the key one β€” if sse4_2 doesn’t appear in /proc/cpuinfo flags, native sharp will never work on that host without compiling from source. πŸŽ―β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹
@American Chinchilla βœ… What works Locally (same code, same S3 image): - `content-type: image/webp` - `content-length: ~135KB` Manual test inside the container: const sharp = require('sharp'); const res = await fetch('IMAGE_URL'); const buf = Buffer.from(await res.arrayBuffer()); const out = await sharp(buf) .resize({ width: 1920 }) .webp({ quality: 60 }) .toBuffer(); console.log(out.length); // ~135KB So: - sharp works - image is valid - conversion to WebP works --- ❌ What does NOT work Production request: `/_next/image?url=...&w=1920&q=60` Response: `content-type: image/jpeg` `content-length: 6714342` `x-nextjs-cache: MISS` Second response: `content-type: image/jpeg` `content-length: 6714342` `x-nextjs-cache: HIT` Even after: `clearing .next/cache/images` `rebuilding container` `hard reload` ---
Palomino
Interesting β€” Sharp Works, But Next.js Doesn’t Use It πŸ€”
This changes everything. Sharp is fine. The problem is upstream of sharp β€” Next.js is either not reaching the optimization step, or something is intercepting/caching the response before Next.js can process it.

Most Likely Culprit: Traefik is Caching/Proxying the Original
The smoking gun is this:

x-nextjs-cache: HIT ← on second request
content-type: image/jpeg
content-length: 6714342 ← identical size, every time


If Next.js were serving this, a HIT would mean it cached the optimized WebP. But you’re getting the original JPEG cached as a HIT β€” which means something between the client and Next.js is:
1. Fetching the raw S3 image directly
2. Caching and returning it, bypassing /_next/image processing entirely
Check your Traefik config for any rules that match /_next/image or proxy .s3.amazonaws.com URLs directly. Also check for a CDN layer (CloudFront, etc.) in front of your domain.

Other High-Probability Causes
1. The url= param is being rewritten
Traefik middleware (stripPrefix, rewrite, redirect) could be transforming the /_next/image?url=... request so Next.js receives a mangled URL β€” causing it to fetch and passthrough the raw image instead of failing loudly.
Add a debug log to confirm what Next.js actually receives:

// middleware.ts (project root)
import { NextRequest, NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
if (req.nextUrl.pathname === '/_next/image') {
console.log('[img-debug]', {
url: req.nextUrl.searchParams.get('url'),
w: req.nextUrl.searchParams.get('w'),
q: req.nextUrl.searchParams.get('q'),
});
}
return NextResponse.next();
}


2. formats: ["image/webp"] requires the client to send Accept: image/webp
Next.js image optimization is content-negotiated β€” it only returns WebP if the request includes Accept: image/webp. If Traefik strips or rewrites the Accept header, Next.js falls back to the original format (JPEG).
Test this directly, bypassing Traefik entirely:

# Hit the container directly (no Traefik)
curl -v \
-H "Accept: image/webp,image/jpeg,
/" \
"http://localhost:3000/_next/image?url=YOUR_ENCODED_S3_URL&w=1920&q=60"


If this returns WebP but the production URL returns JPEG, Traefik is stripping Accept.

3. Standalone output missing sharp in the right place
Even though node -e "require('sharp')" works, standalone mode copies a subset of node_modules. Confirm sharp is actually present in the standalone output:

ls .next/standalone/node_modules | grep sharp
ls .next/standalone/node_modules/sharp/build/Release/


If sharp.node is missing from the standalone copy, Next.js silently falls back. The NEXT_SHARP_PATH env var should handle this, but verify it points to the right binary inside the container:

docker exec <container> ls $NEXT_SHARP_PATH/build/Release/


Definitive Test Order πŸ§ͺ

# 1. Bypass Traefik entirely β€” hit Next.js container directly
curl -H "Accept: image/webp,
/*" \
"http://<container-ip>:3000/_next/image?url=ENCODED_URL&w=1920&q=60" \
-o test.webp -v

# 2. Check what content-type comes back
# If WebP β†’ Traefik is the problem
# If still JPEG β†’ problem is inside Next.js/container

# 3. Confirm sharp path inside container
docker exec <container> node -e "
const p = process.env.NEXT_SHARP_PATH || 'sharp';
const s = require(p);
console.log('sharp ok, version:', s.versions?.sharp);
"


The direct container test (#1) will tell you definitively whether this is a Traefik problem or a Next.js/sharp problem. πŸŽ―β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹
American ChinchillaOP
Thank you for the quick response.

I have managed to bypass traefik and hit Next.js container directly, but it seams the problem is inside Next.js/container.

Here is the tests:
Hitting Next.js container directly:
root@20712429a626:/# curl -v \
  -H 'Accept: image/webp,image/*,*/*' \
  'http://vexen-dev-web-1jvagm:3000/_next/image?url=...&w=1920&q=60'

Response:
- Content-Type: image/jpeg
- Content-Length: 6714342
- X-Nextjs-Cache: MISS β†’ then HIT

So even without Traefik, it returns the original JPEG and caches it.

Confirming sharp path inside container:
root@20712429a626:/#  node -e "
  const p = process.env.NEXT_SHARP_PATH || 'sharp';
  const s = require(p);
  console.log('sharp ok, version:', s.versions?.sharp);
"
sharp ok, version: 0.33.5


Additionaly, I have added this to next.config.ts:
try {
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  const sharp = require(process.env.NEXT_SHARP_PATH || "sharp")
  console.log(":white_check_mark: SHARP OK:", sharp.versions)
} catch (e) {
  console.error(":x: SHARP FAIL:", e)
}


And inside GitHub actions where I have build the image, I got possitive result:
#14 1.157 :white_check_mark: SHARP OK: {
...
#14 1.157   sharp: '0.33.5'
#14 1.157 }
American ChinchillaOP
Moreover, I build simple page to test different type of images (local, remote, remote like in the live app, and with custom loader)
...
<Image src="/couch_lights_on.jpg" alt="Just Image" width={800} height={600} quality={75} />
...
<Image src="https://dev-vexen.s3.eu-central-1.amazonaws.com/f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg" alt="Just Image" width={800} height={600} quality={75} />
...
<Image src={getUrlByName("f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg")} alt="Hero Banner" fill sizes="900px" quality={60} loading="eager" fetchPriority="high" className="object-cover -z-20 md:rounded-[12px]" />
...
<Image src={`/api/image?url=${encodeURIComponent("https://dev-vexen.s3.eu-central-1.amazonaws.com/f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg")}&w=900&q=60`} alt="Hero Banner" width={900} height={609} unoptimized />
...


Custom route:
import sharp from "sharp"

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url)
  const url = searchParams.get("url")
  const w = Number(searchParams.get("w") || "1080")
  const q = Number(searchParams.get("q") || "75")

  if (!url) {
    return new Response("Missing url", { status: 400 })
  }

  const upstream = await fetch(url, {
    cache: "force-cache"
  })

  if (!upstream.ok) {
    return new Response("Failed to fetch source image", { status: 502 })
  }

  const input = Buffer.from(await upstream.arrayBuffer())

  const output = await sharp(input).resize({ width: w, withoutEnlargement: true }).webp({ quality: q }).toBuffer()

  return new Response(new Uint8Array(output), {
    status: 200,
    headers: {
      "Content-Type": "image/webp",
      "Cache-Control": "public, max-age=31536000, immutable"
    }
  })
}


Now I see in the logs error:
Failed to set Next.js data cache for https://.../f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg, items over 2MB can not be cached (8953121 bytes)


The Static image and Remote image with custome loader were optimized.
@American Chinchilla Thank you for the quick response. I have managed to bypass traefik and hit Next.js container directly, but it seams the problem is inside Next.js/container. Here is the tests: Hitting Next.js container directly: root@20712429a626:/# curl -v \ -H 'Accept: image/webp,image/*,*/*' \ 'http://vexen-dev-web-1jvagm:3000/_next/image?url=...&w=1920&q=60' Response: - `Content-Type: image/jpeg` - `Content-Length: 6714342` - `X-Nextjs-Cache: MISS` β†’ then `HIT` So even without Traefik, it returns the original JPEG and caches it. Confirming sharp path inside container: root@20712429a626:/# node -e " const p = process.env.NEXT_SHARP_PATH || 'sharp'; const s = require(p); console.log('sharp ok, version:', s.versions?.sharp); " sharp ok, version: 0.33.5 Additionaly, I have added this to `next.config.ts`: ts try { // eslint-disable-next-line @typescript-eslint/no-require-imports const sharp = require(process.env.NEXT_SHARP_PATH || "sharp") console.log(":white_check_mark: SHARP OK:", sharp.versions) } catch (e) { console.error(":x: SHARP FAIL:", e) } And inside GitHub actions where I have build the image, I got possitive result: #14 1.157 :white_check_mark: SHARP OK: { ... #14 1.157 sharp: '0.33.5' #14 1.157 }
Palomino
Sharp loads, works manually, but Next.js silently falls back.

Most likely: Next.js’s internal S3 fetch returns something unexpected (redirect, wrong content-type, chunked encoding) before it even hits sharp.

Run this one command and paste the output πŸ‘‡

curl -Iv "YOUR_FULL_S3_URL" 2>&1 | grep -E "HTTP|content-type|content-length|location|transfer-encoding|content-encoding"
@American Chinchilla Moreover, I build simple page to test different type of images (local, remote, remote like in the live app, and with custom loader) tsx ... <Image src="/couch_lights_on.jpg" alt="Just Image" width={800} height={600} quality={75} /> ... <Image src="https://dev-vexen.s3.eu-central-1.amazonaws.com/f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg" alt="Just Image" width={800} height={600} quality={75} /> ... <Image src={getUrlByName("f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg")} alt="Hero Banner" fill sizes="900px" quality={60} loading="eager" fetchPriority="high" className="object-cover -z-20 md:rounded-[12px]" /> ... <Image src={`/api/image?url=${encodeURIComponent("https://dev-vexen.s3.eu-central-1.amazonaws.com/f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg")}&w=900&q=60`} alt="Hero Banner" width={900} height={609} unoptimized /> ... Custom route: import sharp from "sharp" export async function GET(req: Request) { const { searchParams } = new URL(req.url) const url = searchParams.get("url") const w = Number(searchParams.get("w") || "1080") const q = Number(searchParams.get("q") || "75") if (!url) { return new Response("Missing url", { status: 400 }) } const upstream = await fetch(url, { cache: "force-cache" }) if (!upstream.ok) { return new Response("Failed to fetch source image", { status: 502 }) } const input = Buffer.from(await upstream.arrayBuffer()) const output = await sharp(input).resize({ width: w, withoutEnlargement: true }).webp({ quality: q }).toBuffer() return new Response(new Uint8Array(output), { status: 200, headers: { "Content-Type": "image/webp", "Cache-Control": "public, max-age=31536000, immutable" } }) } Now I see in the logs error: Failed to set Next.js data cache for https://.../f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg, items over 2MB can not be cached (8953121 bytes) The Static image and Remote image with custome loader were optimized.
Palomino
Add this to next.config.ts πŸ‘‡

cacheMaxMemorySize: 0,


Next.js tries to cache the raw S3 image fetch (8.9MB) β†’ exceeds 2MB limit β†’ silently falls back to original. Setting it to 0 disables the in-memory cache and lets optimization proceed normally. βœ…β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹
@Palomino Sharp loads, works manually, but Next.js silently falls back. Most likely: Next.js’s internal S3 fetch returns something unexpected (redirect, wrong content-type, chunked encoding) before it even hits sharp. Run this one command and paste the output πŸ‘‡ curl -Iv "YOUR_FULL_S3_URL" 2>&1 | grep -E "HTTP|content-type|content-length|location|transfer-encoding|content-encoding"
American ChinchillaOP
# curl -Iv "https://dev-vexen.s3.eu-central-1.amazonaws.com/f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg" 2>&1 | grep -E "HTTP|content-type|content-length|location|transfer-encoding|content-encoding"
* using HTTP/1.1
> HEAD /f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg HTTP/1.1
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
American ChinchillaOP
I confirmed the /_next/image request inside Next middleware. It receives:

accept: image/avif,image/webp,...
correct url
correct w / q

So Accept forwarding is not the issue.

Also, while handling /_next/image, the container logs show:

WARNING: CPU supports 0x6000000000004000, software requires 0x4000000000005000

At the same time:

manual sharp(...).webp() inside the same container works
/_next/image still returns JPEG

So it looks like Next’s image optimizer path is still hitting a CPU-incompatible binary/runtime path and silently falling back, even though manual sharp conversion works.

Debug route:
// app/api/debug-accept/route.ts:
import { headers } from "next/headers"

export async function GET() {
  const h = await headers()

  return Response.json({
    accept: h.get("accept"),
    host: h.get("host"),
    xForwardedProto: h.get("x-forwarded-proto")
  })
}


Result:
{"accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","host":"dev.vexen.eu","xForwardedProto":"https"}


middleware.ts:
...
  if (nextUrl.pathname === "/_next/image") {
    console.log("[_next/image]", {
      accept: req.headers.get("accept"),
      url: nextUrl.searchParams.get("url"),
      w: nextUrl.searchParams.get("w"),
      q: nextUrl.searchParams.get("q"),
      host: req.headers.get("host"),
      xForwardedProto: req.headers.get("x-forwarded-proto")
    })
    return null
  }
...


result:
[_next/image] {
accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
url: 'https://dev-vexen.s3.eu-central-1.amazonaws.com/f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg',
w: '1920',
q: '75',
host: 'dev.vexen.eu',
xForwardedProto: 'https'
}


At this point, does this look like a known Next.js image optimizer issue with older CPUs / standalone mode, or is there another runtime path I should inspect?
@American Chinchilla I confirmed the /_next/image request inside Next middleware. It receives: accept: image/avif,image/webp,... correct url correct w / q So Accept forwarding is not the issue. Also, while handling /_next/image, the container logs show: WARNING: CPU supports 0x6000000000004000, software requires 0x4000000000005000 At the same time: manual sharp(...).webp() inside the same container works /_next/image still returns JPEG So it looks like Next’s image optimizer path is still hitting a CPU-incompatible binary/runtime path and silently falling back, even though manual sharp conversion works. Debug route: ts // app/api/debug-accept/route.ts: import { headers } from "next/headers" export async function GET() { const h = await headers() return Response.json({ accept: h.get("accept"), host: h.get("host"), xForwardedProto: h.get("x-forwarded-proto") }) } Result: {"accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","host":"dev.vexen.eu","xForwardedProto":"https"} middleware.ts: ts ... if (nextUrl.pathname === "/_next/image") { console.log("[_next/image]", { accept: req.headers.get("accept"), url: nextUrl.searchParams.get("url"), w: nextUrl.searchParams.get("w"), q: nextUrl.searchParams.get("q"), host: req.headers.get("host"), xForwardedProto: req.headers.get("x-forwarded-proto") }) return null } ... result: [_next/image] { accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', url: 'https://dev-vexen.s3.eu-central-1.amazonaws.com/f2a9056a5601a9367a749be3e4e50590aa2cbaa3.jpg', w: '1920', q: '75', host: 'dev.vexen.eu', xForwardedProto: 'https' } At this point, does this look like a known Next.js image optimizer issue with older CPUs / standalone mode, or is there another runtime path I should inspect?
Palomino
RUN cp -r /app/node_modules/sharp \
/app/.next/standalone/node_modules/sharp


Next.js standalone has its own node_modules and ignores NEXT_SHARP_PATH β€” it resolves a different (CPU-incompatible) sharp binary internally. This forces it to use your working one. βœ…β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹
Put this inside your Docker file
American ChinchillaOP
Hi, I tried the suggested Docker fix, but the build fails during CD.
#20 [runner  7/11] RUN cp -r /app/node_modules/sharp     /app/.next/standalone/node_modules/sharp
#20 0.153 cp: cannot create directory '/app/.next/standalone/node_modules/sharp': No such file or directory
#20 ERROR: process "/bin/sh -c cp -r /app/node_modules/sharp     /app/.next/standalone/node_modules/sharp" did not complete successfully: exit code: 1
------
 > [runner  7/11] RUN cp -r /app/node_modules/sharp     /app/.next/standalone/node_modules/sharp:
0.153 cp: cannot create directory '/app/.next/standalone/node_modules/sharp': No such file or directory
------
Dockerfile:41
--------------------
  40 |     
  41 | >>> RUN cp -r /app/node_modules/sharp \
  42 | >>>     /app/.next/standalone/node_modules/sharp
  43 |     
--------------------
ERROR: failed to build: failed to solve: process "/bin/sh -c cp -r /app/node_modules/sharp     /app/.next/standalone/node_modules/sharp" did not complete successfully: exit code: 1


So in the runner stage, /app/.next/standalone/node_modules does not exist.

Because I copy .next/standalone into /app with:
COPY --from=builder /app/.next/standalone ./


I think there is no /app/.next/standalone/... path in the final image.

Should this copy step be done in the builder stage instead, before COPY --from=builder /app/.next/standalone ./? Or should I be copying sharp into a different path in the runner stage?
# FROM node:20-alpine AS base
FROM node:24 AS base
LABEL org.opencontainers.image.source=https://github.com/AndreyPerunov/vexen

RUN apt-get update -y \
 && apt-get install -y openssl \
 && rm -rf /var/lib/apt/lists/*

# Stage 1: Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Stage 2: Build the application
FROM base AS builder
WORKDIR /app

ARG NEXT_PUBLIC_IMAGES_PATH
ARG NEXT_PUBLIC_GTM_ID
ENV NEXT_PUBLIC_GTM_ID=$NEXT_PUBLIC_GTM_ID
ENV NEXT_PUBLIC_IMAGES_PATH=$NEXT_PUBLIC_IMAGES_PATH

COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build

RUN mkdir -p /app/.next/standalone/node_modules
RUN rm -rf /app/.next/standalone/node_modules/sharp
RUN cp -r /app/node_modules/sharp /app/.next/standalone/node_modules/sharp

# Stage 3: Production server
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_SHARP_PATH=/app/node_modules/sharp

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules ./node_modules

# Create logs directory
RUN mkdir -p /app/logs
RUN mkdir -p /app/logs/users
RUN touch /app/logs/web.log
RUN touch /app/logs/users/user.log

EXPOSE 3000
CMD ["sh","-c","npx prisma migrate deploy && exec node server.js 2>&1 | tee -a /app/logs/web.log"]
@American Chinchilla Hi, I tried the suggested Docker fix, but the build fails during CD. #20 [runner 7/11] RUN cp -r /app/node_modules/sharp /app/.next/standalone/node_modules/sharp #20 0.153 cp: cannot create directory '/app/.next/standalone/node_modules/sharp': No such file or directory #20 ERROR: process "/bin/sh -c cp -r /app/node_modules/sharp /app/.next/standalone/node_modules/sharp" did not complete successfully: exit code: 1 ------ > [runner 7/11] RUN cp -r /app/node_modules/sharp /app/.next/standalone/node_modules/sharp: 0.153 cp: cannot create directory '/app/.next/standalone/node_modules/sharp': No such file or directory ------ Dockerfile:41 -------------------- 40 | 41 | >>> RUN cp -r /app/node_modules/sharp \ 42 | >>> /app/.next/standalone/node_modules/sharp 43 | -------------------- ERROR: failed to build: failed to solve: process "/bin/sh -c cp -r /app/node_modules/sharp /app/.next/standalone/node_modules/sharp" did not complete successfully: exit code: 1 So in the `runner` stage, `/app/.next/standalone/node_modules` does not exist. Because I copy `.next/standalone` into `/app` with: docker COPY --from=builder /app/.next/standalone ./ I think there is no `/app/.next/standalone/...` path in the final image. Should this copy step be done in the builder stage instead, before `COPY --from=builder /app/.next/standalone ./`? Or should I be copying `sharp` into a different path in the runner stage?
Palomino
Do it in the builder stage, before the runner copies happen πŸ‘‡

# In builder stage, after next build:
RUN cp -r /app/node_modules/sharp /app/.next/standalone/node_modules/sharp


Then your existing runner COPY will include sharp automatically. βœ…β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹β€‹