이 기사는 현재 영어로만 제공됩니다.

DockerApr 5, 20267 min read

Sharp Not Working in Alpine Docker? Here's a Zero-Dependency Fix

Jack avatar
JackAuthor

Sharp Not Working in Alpine Docker? Here's a Zero-Dependency Fix

You picked Alpine for a reason. Small images, fast pulls, minimal attack surface. Then you added Sharp to your image pipeline and everything fell apart.

If you're here, you've probably already spent 30 minutes staring at one of these:

Error: Could not load the "sharp" module using the linux-x64 runtime

Possible solutions:
- Ensure optional dependencies can be installed:
    npm install --include=optional sharp
- Ensure your package manager supports multi-platform installation

Or the classic node-gyp wall of text:

npm ERR! sharp@0.33.5 install: `node-gyp rebuild`
npm ERR! gyp ERR! find Python
npm ERR! gyp ERR! find Python - "python3" is not in PATH or produced an error
npm ERR! gyp ERR! build error

Or perhaps the most frustrating variant, where the build succeeds but crashes at runtime:

Error: /usr/lib/libvips.so.42: undefined symbol: g_string_free_and_steal

This article explains why Sharp breaks in Alpine Docker, walks through the common workarounds and their trade-offs, and then shows you an alternative that sidesteps the problem entirely.

The Typical Broken Dockerfile#

Here's what most developers start with. It looks clean. It doesn't work.

FROM node:22-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

# This will fail at build time or crash at runtime
RUN node scripts/optimize-images.js

Where optimize-images.js does something like:

const sharp = require('sharp');

(async () => {
  await sharp('input.jpg')
    .webp({ quality: 80 })
    .toFile('output.webp');
})();

The npm ci step either fails outright, or it installs successfully but the module crashes when Node actually tries to load the native binary. Either way, your CI pipeline is broken and your Friday afternoon is gone.

Why Sharp Breaks in Docker (Especially Alpine)#

Sharp is a high-performance image processing library, and its speed comes from libvips, a native C library. That native dependency is exactly where things go wrong in containers.

The musl vs glibc problem. Alpine Linux uses musl libc instead of the glibc that most Linux distributions ship. Sharp's prebuilt binaries are compiled against glibc. When Node tries to load them on Alpine, the dynamic linker can't resolve the symbols. The binary is there, but it's incompatible at the ABI level.

The build-from-source fallback. When prebuilt binaries don't match your platform, Sharp falls back to compiling libvips from source using node-gyp. This requires a full C/C++ toolchain: python3, make, g++, and the libvips development headers. Alpine doesn't ship any of these by default.

The dependency chain. Even if you install the build tools, libvips itself depends on a long list of libraries: libjpeg-turbo, libpng, libwebp, libexif, glib, and more. Each one needs to be present at both build time and runtime.

The multi-architecture trap. If you build your image on an M1/M2 Mac (ARM64) but deploy to x86_64 servers, Sharp will download ARM binaries during npm ci that won't work in the target architecture. Docker's --platform flag helps, but it introduces emulation overhead and its own class of bugs.

Common Workarounds (and Why They're Fragile)#

The internet has plenty of advice for fixing Sharp in Alpine. Most of it works today and breaks next month.

Workaround 1: Install all the native dependencies#

FROM node:22-alpine

RUN apk add --no-cache \
    vips-dev \
    build-base \
    python3 \
    pkgconfig

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

This works, but it adds roughly 150MB to your image. Your lean 50MB Alpine image is now 200MB. You've traded the small-image benefit of Alpine for... Alpine. At that point, node:22-slim (Debian-based) would give you the same thing with fewer surprises.

Workaround 2: Multi-stage build#

FROM node:22-alpine AS builder

RUN apk add --no-cache vips-dev build-base python3
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN node scripts/optimize-images.js

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist

Better. The final image is small again. But now you have a more complex Dockerfile, longer build times, and a builder stage that still needs all those native dependencies. If you're processing images at runtime (not just build time), this approach doesn't help at all.

Workaround 3: Force platform-specific binaries#

npm install --os=linux --cpu=x64 sharp

This tells npm to download the x64 Linux prebuilt binary regardless of what platform you're currently on. It works until Sharp releases a new version that changes the binary layout, or until you switch to ARM-based CI runners, or until someone on your team runs npm ci on their Mac and overwrites the lockfile.

Workaround 4: Use node:slim instead of Alpine#

FROM node:22-slim

Sharp works out of the box on Debian-based images because they ship glibc. But node:22-slim is ~200MB compared to Alpine's ~50MB. If you chose Alpine deliberately for its small footprint, switching to Debian defeats the purpose.

The pattern#

Every workaround either increases your image size, adds build complexity, or introduces platform-specific fragility. The fundamental problem remains: you're shipping a native binary that must match the exact OS, libc, and CPU architecture of your runtime environment.

The GetWebP Alternative: Pure WebAssembly, Zero Native Dependencies#

GetWebP takes a different approach. Instead of wrapping a native C library, it compiles the image codec directly to WebAssembly. The WASM binary runs inside Node's V8 engine, the same way your JavaScript does. No native binaries, no C toolchain, no platform-specific builds.

Here's the same image optimization task in Alpine Docker:

FROM node:22-alpine

WORKDIR /app
COPY . .
RUN npx getwebp ./images --quality 80

That's it. No apk add. No build-base. No python3. No multi-stage build. The Alpine image stays small, the Dockerfile stays simple, and it works identically on x86_64, ARM64, macOS, and Windows.

Why this works everywhere:

  • WebAssembly is a platform-independent bytecode format. The same .wasm file runs on any architecture.
  • Node.js has built-in WASM support since v8. No additional runtime required.
  • musl vs glibc is irrelevant. WASM doesn't use the system's C library.
  • The entire GetWebP package adds about 5MB to your image. Compare that to 150MB+ of native dependencies.

The Dockerfile above is production-ready. It works on GitHub Actions, GitLab CI, AWS CodeBuild, and every other CI system that supports Docker. No special configuration, no platform matrix, no conditional apk add blocks.

Benchmark Comparison: Speed vs. Simplicity#

Let's be honest about the trade-off. Sharp is faster. It's running optimized C code through libvips, and that raw performance advantage is real.

Here are benchmarks from real-world image sizes:

ImageInputGetWebPSharpImageMagick
320x240 JPEG40 KB206ms, 11 KB89ms, 11 KB33ms, 11 KB
1920x1080 JPEG768 KB643ms, 163 KB331ms, 163 KB276ms, 163 KB
4096x3072 JPEG3.9 MB2736ms, 730 KB1196ms, 730 KB1250ms, 730 KB

A few things to note:

Output sizes are identical between GetWebP and Sharp at the same quality setting. GetWebP produces the same compressed output as Sharp when using the same quality parameter. The WebP encoder is the same algorithm; only the execution layer differs.

GetWebP is roughly 2x slower in raw conversion time. For a single large image, that's about 1.5 extra seconds. For a batch of typical web images (under 1080p), the difference is under a second per image.

Now consider the full picture. How much time have you spent debugging node-gyp rebuild? How long did it take to figure out the musl/glibc incompatibility? How many CI minutes were burned on failed builds?

For Docker builds and CI pipelines, the 1-2 second difference per image is negligible. The minutes (or hours) saved from not debugging native dependency issues is where the real time savings live.

If you're running a high-throughput image processing service handling thousands of images per second, Sharp's raw speed matters. If you're optimizing assets during a Docker build or processing uploads in a web app, GetWebP's reliability and simplicity matter more.

Quick Start#

Process an entire directory of images in one command. No install step, no configuration file, no dependencies to manage:

# One line. No install. No dependencies.
npx getwebp ./images

Customize quality and target specific formats:

# Convert all JPG and PNG files at quality 85
npx getwebp ./images --quality 85

# Recursive conversion
npx getwebp ./src/assets --quality 80 -r

Use it in your Dockerfile:

FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npx getwebp ./public/images --quality 80

Use it in your CI pipeline:

# GitHub Actions
- name: Optimize images
  run: npx getwebp ./public/images --quality 80

No setup step. No OS-specific conditionals. It just works.


Ready to stop fighting with native dependencies? Check out the documentation for the full feature set, or see pricing for team and commercial usage.

Jack avatar

Jack

Author