Next.js Image Optimization returns original JPEG in production (self-hosted, standalone, S3)
Unanswered
American Chinchilla posted this in #help-forum
American ChinchillaOP
Hi everyone π
Iβm running a self-hosted Next.js app (v15.5.13) with
---
β οΈ Problem
In production, requests to
-
-
-
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
Docker (Simplified):
---
β 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!
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: MISSExpected 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):
-
-
Manual test inside the container:
So:
- sharp works
- image is valid
- conversion to WebP works
---
β What does NOT work
Production request:
Response:
Second response:
Even after:
---
Locally (same code, same S3 image):
-
content-type: image/webp-
content-length: ~135KBManual 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); // ~135KBSo:
- sharp works
- image is valid
- conversion to WebP works
---
β What does NOT work
Production request:
/_next/image?url=...&w=1920&q=60Response:
content-type: image/jpegcontent-length: 6714342x-nextjs-cache: MISSSecond response:
content-type: image/jpegcontent-length: 6714342x-nextjs-cache: HITEven after:
clearing .next/cache/imagesrebuilding containerhard 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 |
|-------------------|-----------------------------------------------------|
|
|
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',
},
});
}
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. π―ββββββββββββββββ
[ ] 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. π―ββββββββββββββββ
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:
Response:
-
-
-
So even without Traefik, it returns the original JPEG and caches it.
Confirming sharp path inside container:
Additionaly, I have added this to
And inside GitHub actions where I have build the image, I got possitive result:
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 HITSo 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.5Additionaly, 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)
Custom route:
Now I see in the logs error:
The Static image and Remote image with custome loader were optimized.
...
<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"
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. β ββββββββββββββββ
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 OKAmerican 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:
Result:
middleware.ts:
result:
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?
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. β ββββββββββββββββ
/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.
So in the
Because I copy
I think there is no
Should this copy step be done in the builder stage instead, before
#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: 1So 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
RUN cp -r /app/node_modules/sharp /app/.next/standalone/node_modules/sharp
Then your existing runner COPY will include sharp automatically. β ββββββββββββββββ
# 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. β ββββββββββββββββ