← Back to posts
comparison guide self-hosted · · 15 min read

Self-Hosted Image Optimization 2026: imgproxy vs Thumbor vs Sharp

A comprehensive guide to self-hosted image optimization services in 2026. Compare imgproxy, Thumbor, and Sharp-based solutions with Docker setup instructions and performance benchmarks.

OS
Editorial Team

Every modern website and application serves images — product photos, avatars, blog thumbnails, hero banners, and more. The problem? Serving unoptimized images is the single biggest cause of slow page loads. A single 5 MB photo from a smartphone camera can take seconds to load on a mobile connection, driving away visitors before they even see your content.

Cloud services like Cloudinary, Imgix, and ImageKit solve this by providing on-the-fly image transformation APIs. You upload the original, request a resized version via URL parameters, and the service delivers an optimized file. But what if you don’t want your images flowing through a third-party server? What if you need full control over pricing, performance, and data residency?

This guide covers the best open-source, self-hosted image optimization services available in 2026: imgproxy, Thumbor, and Sharp-based custom solutions. Each offers real-time image resizing, format conversion, compression, and advanced transformations — all running on your own infrastructure.

Why Self-Host Your Image Optimization

Running your own image processing service delivers advantages that go well beyond privacy:

No per-operation pricing. Cloud image services charge per transformation, per GB of bandwidth, or per stored image. A growing media site can easily spend hundreds of dollars a month on image processing alone. Self-hosted solutions cost only the compute resources you allocate — a $10/month VPS can handle thousands of image requests per day.

Lower latency on your own CDN. When the image processor runs alongside your application or on the same network as your CDN origin, transformed images are generated closer to your users. You eliminate the extra network hop to a third-party service’s edge nodes.

Full format support. Self-hosted tools let you serve the most modern image formats — AVIF and JPEG XL — without waiting for a cloud provider to add support. You control which codecs are available and which compression settings are used.

No vendor lock-in. Your image URLs point to your own domain. If you switch tools later, the migration is a matter of URL rewriting, not re-uploading every image to a new provider.

Compliance and data sovereignty. For applications handling user-uploaded content — healthcare, legal, or enterprise platforms — keeping image processing in-house means image data never leaves your controlled environment.

imgproxy: The Fastest Option

imgproxy is a blazing-fast, standalone image processing server written in Go. It was designed from the ground up for speed and security, using libvips as its image processing engine. libvips is the same backend used by Sharp, and it’s significantly faster and more memory-efficient than ImageMagick.

imgproxy works by taking a source image URL, applying transformations specified in the URL path, and returning the processed image. It supports caching, watermarking, face detection, and more than 30 image formats.

Key Features

  • Lightning-fast processing — libvips backend processes images 4–8x faster than ImageMagick-based tools
  • Security-first design — URL signatures prevent unauthorized transformations and hotlinking
  • AVIF and WebP support — automatic format negotiation based on browser Accept headers
  • Smart cropping — detects focal points (faces, interesting regions) for intelligent crop targets
  • Health monitoring — built-in health check endpoint for load balancer integration
  • Minimal resource usage — a single instance uses ~50 MB RAM at idle

docker Setup

The simplest way to run imgproxy is via Docker:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: "3.8"
services:
  imgproxy:
    image: darthsim/imgproxy:latest
    container_name: imgproxy
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      # Bind to all interfaces inside the container
      IMGPROXY_BIND: ":8080"
      # Base URL for source images (your application's upload directory)
      IMGPROXY_SOURCE_URLS: "https://your-app.com/uploads/"
      # Enable AVIF and WebP output
      IMGPROXY_AVIF_QUALITY: 80
      IMGPROXY_WEBP_QUALITY: 80
      IMGPROXY_JPEG_QUALITY: 85
      # URL signature key (generate with: openssl rand -hex 32)
      IMGPROXY_KEY: "your-secret-key-hex"
      IMGPROXY_SALT: "your-salt-hex"
      # Concurrency tuning
      IMGPROXY_CONCURRENCY: 4
      IMGPROXY_MAX_CLIENTS: 1000
      # Cache settings
      IMGPROXY_TTL: 31536000
      IMGPROXY_CACHE_CONTROL_PASSTHROUGH: false
    volumes:
      # Optional: mount a local image source directory
      - ./images:/var/local/images

Start the service:

1
docker compose up -d

URL Format and Usage

imgproxy encodes all transformation parameters directly in the URL path. A typical image URL looks like this:

1
https://imgproxy.example.com/unsafe/rs:fit:800:600/plain/https://your-app.com/uploads/photo.jpg@webp

Breaking down the components:

SegmentMeaning
unsafeNo signature verification (use signed URLs in production)
rs:fit:800:600Resize to fit within 800×600, preserving aspect ratio
plainURL encoding mode (plain text after this point)
https://...Source image URL
@webpOutput format override

Common resize modes:

1
2
3
4
rs:fit:800:600      # Fit within bounds, preserve aspect ratio
rs:fill:800:600     # Fill the bounds, may crop edges
r:800:600           # Exact resize (distorts if ratio differs)
th:200              # Thumbnail: resize and crop to exactly 200×200

For production, always use signed URLs. imgproxy provides a CLI tool for generating them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Install the CLI tool
go install github.com/imgproxy/imgproxy-cli@latest

# Generate a signed URL
imgproxy url \
  --key "your-secret-key" \
  --salt "your-salt" \
  --resize fit:800:600 \
  --format webp \
  https://your-app.com/uploads/photo.jpg

This outputs a URL like:

1
https://imgproxy.example.com/AbCdEfGh.../rs:fit:800:600/plain/https://your-app.com/uploads/photo.jpg@webp

Performance Tuning

For high-traffic deployments, tune these environment variables:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
environment:
  # Number of goroutines for image processing
  IMGPROXY_CONCURRENCY: 8
  # Maximum concurrent image downloads
  IMGPROXY_DOWNLOAD_CONCURRENCY: 4
  # Limit image dimensions to prevent abuse
  IMGPROXY_MAX_SRC_RESOLUTION: 50        # Megapixels
  IMGPROXY_MAX_SRC_FILE_SIZE: 52428800   # 50 MB
  # Memory limits
  IMGPROXY_FREE_MEMORY_INTERVAL: 15      # Seconds between memory frees
  IMGPROXY_FREE_MEMORY_OS: true          # Return memory to OS

A well-tuned single imgproxy instance on a 4-core VPS can handle 500+ requests per second for cached transformations.

Thumbor: The Feature-Rich Veteran

Thumbor is a Python-based image processing service that has been around since 2012. While imgproxy focuses on raw speed, Thumbor prioritizes flexibility — it offers a plugin architecture that lets you extend almost every part of the processing pipeline.

Thumbor uses a detection-and-transformation model: it first analyzes the image (detecting faces, focal points, and features), then applies transformations based on that analysis. This makes it particularly strong for user-generated content where automatic cropping and smart resizing matter.

Key Features

  • Smart detection — face detection, feature point detection, and automatic focal point identification
  • Plugin ecosystem — dozens of community plugins for storage, detection, filters, and result caching
  • Flexible storage backends — store source and transformed images on filesystem, S3, Google Cloud Storage, or any custom backend
  • Rich filter library — watermark, blur, sharpen, color filters, text overlays, and more
  • Auto WebP/AVIF — content negotiation for modern formats
  • Security — HMAC-signed URLs with configurable token expiration

Docker Setup

Thumbor’s official Docker image includes the core service and common plugins:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
version: "3.8"
services:
  thumbor:
    image: minimalcompact/thumbor:latest
    container_name: thumbor
    restart: unless-stopped
    ports:
      - "8888:8888"
    environment:
      # Security key for URL signing (generate with: openssl rand -base64 32)
      SECURITY_KEY: "my-secret-security-key-base64"
      # Storage configuration
      STORAGE: "thumbor.storages.file_storage"
      FILE_STORAGE_ROOT_PATH: "/data/thumbor/storage"
      # Result storage (caching layer)
      RESULT_STORAGE: "thumbor.result_storages.file_storage"
      RESULT_STORAGE_FILE_STORAGE_ROOT_PATH: "/data/thumbor/cache"
      # Image processing settings
      QUALITY: 85
      AUTO_WEBP: true
      AUTO_AVIF: true
      # Maximum image dimensions
      MAX_WIDTH: 2000
      MAX_HEIGHT: 2000
      # Allow animated GIFs
      ANIMATED_GIFS_DETECTION: true
      # Enable face detection
      FACE_DETECTOR_CASCADE_FILE: "/usr/local/lib/python3.11/site-packages/thumbor/detectors/face_detector/cascades/haarcascade_frontalface_default.xml"
    volumes:
      - ./thumbor_data:/data/thumbor
      # Optional: mount source images
      - ./images:/data/source_images

URL Format and Usage

Thumbor URLs follow a different pattern than imgproxy:

1
https://thumbor.example.com/unsafe/800x600/smart/https://your-app.com/uploads/photo.jpg

Components:

SegmentMeaning
unsafeNo signature (replace with HMAC hash in production)
800x600Target dimensions
smartUse smart cropping (face/feature detection)
URLSource image path or URL

Thumbor’s URL structure is more readable but less expressive than imgproxy’s. Filters are appended as a separate segment:

1
https://thumbor.example.com/unsafe/300x200/filters:watermark(/logo.png,10,10,50):brightness(20)/photo.jpg

Common filters:

1
2
3
4
5
6
7
filters:watermark(url,x,y,transparency)   # Add watermark overlay
filters:brightness(value)                  # Adjust brightness (-100 to 100)
filters:contrast(value)                    # Adjust contrast
filters:blur(sigma)                        # Gaussian blur
filters:sharpen(amount,width,height)       # Sharpen image
filters:round_corner(radius,color)         # Rounded corners
filters:no_upscale()                       # Don't enlarge small images

Extending Thumbor with Plugins

Thumbor’s real power comes from its plugin system. Install community plugins via pip:

1
2
3
4
5
6
FROM minimalcompact/thumbor:latest

RUN pip install \
    tc-aws \                    # AWS S3 storage backend
    thumbor-plugins-aws-sns \   # SNS notifications
    thumbor-plugins-optimize    # Additional optimization filters

Custom storage plugin configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
environment:
  # Use S3 for source images
  STORAGE: "tc_aws.storages.s3_storage"
  TC_AWS_REGION: "us-east-1"
  TC_AWS_STORAGE_BUCKET: "my-source-images"
  TC_AWS_STORAGE_ROOT_PATH: "originals"
  # Use S3 for cached results
  RESULT_STORAGE: "tc_aws.result_storages.s3_storage"
  TC_AWS_RESULT_STORAGE_BUCKET: "my-image-cache"
  TC_AWS_RESULT_STORAGE_ROOT_PATH: "thumbs"

Sharp-Based Custom Solutions

Sharp is a high-performance Node.js image processing library built on libvips. Unlike imgproxy and Thumbor, Sharp is not a ready-to-run service — it’s a library you embed in your own application. This makes it the ideal choice when you need image processing tightly integrated with your existing codebase.

Sharp is the engine that powers Next.js Image, Gatsby Image, and many other popular web frameworks. If your application is already built with Node.js, using Sharp directly gives you full programmatic control over every transformation.

Key Features

  • Library, not a service — embed directly in your application, no separate process
  • Full programmatic control — chain transformations in code with TypeScript/JavaScript
  • Stream-based processing — process images as streams, ideal for large files
  • Framework integration — native support in Next.js, Gatsby, Astro, and Nuxt
  • Same libvips backend — identical performance and format support to imgproxy
  • Zero external dependencies — no need to manage a separate image service container

When to Choose Sharp Over imgproxy or Thumbor

ScenarioBest Choice
Separate microservice for image processingimgproxy
Smart cropping and face detection neededThumbor
Already running Node.js/TypeScript appSharp
Using Next.js, Gatsby, or AstroSharp (built-in)
Need custom business logic per imageSharp
Want plug-and-play with URL-based APIimgproxy or Thumbor

Docker Setup with Express.js

Here’s a complete self-hosted image service built with Sharp and Express:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
FROM node:20-slim

WORKDIR /app

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

COPY package*.json ./
RUN npm install --production

COPY server.js ./
RUN mkdir -p /app/uploads /app/cache

EXPOSE 3000
CMD ["node", "server.js"]
1
2
3
4
5
6
7
8
9
{
  "name": "image-service",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.21.0",
    "sharp": "^0.34.0",
    "crypto": "^1.0.1"
  }
}

The Express server implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const express = require("express");
const sharp = require("sharp");
const crypto = require("crypto");
const path = require("path");
const fs = require("fs");

const app = express();
const PORT = process.env.PORT || 3000;
const SECRET = process.env.SECRET_KEY || "change-me";
const UPLOAD_DIR = path.join(__dirname, "uploads");
const CACHE_DIR = path.join(__dirname, "cache");

// Ensure directories exist
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
fs.mkdirSync(CACHE_DIR, { recursive: true });

// Generate cache key from transformation parameters
function cacheKey(name, options) {
  const key = `${name}-${JSON.stringify(options)}`;
  return crypto.createHash("md5").update(key).digest("hex");
}

// Parse and validate image transformation URL
// Format: /image/{width}x{height}/{format}/{filename}
app.get("/image/:width/:height/:format/:filename", async (req, res) => {
  const { width, height, format, filename } = req.params;
  const w = parseInt(width, 10);
  const h = parseInt(height, 10);

  // Validate dimensions
  if (isNaN(w) || isNaN(h) || w < 1 || h < 1 || w > 4000 || h > 4000) {
    return res.status(400).json({ error: "Invalid dimensions" });
  }

  // Validate format
  const allowedFormats = ["jpeg", "webp", "avif", "png"];
  if (!allowedFormats.includes(format)) {
    return res.status(400).json({ error: "Unsupported format" });
  }

  // Check source file exists
  const sourcePath = path.join(UPLOAD_DIR, filename);
  if (!fs.existsSync(sourcePath)) {
    return res.status(404).json({ error: "Image not found" });
  }

  // Check cache
  const cachePath = path.join(
    CACHE_DIR,
    `${cacheKey(filename, { w, h, format })}.${format}`
  );
  if (fs.existsSync(cachePath)) {
    res.set("Content-Type", `image/${format}`);
    res.set("Cache-Control", "public, max-age=31536000");
    return res.sendFile(cachePath);
  }

  try {
    // Process image with Sharp
    let pipeline = sharp(sourcePath).resize(w, h, {
      fit: "inside",
      withoutEnlargement: true,
    });

    // Apply format-specific options
    if (format === "jpeg") {
      pipeline = pipeline.jpeg({ quality: 85, mozjpeg: true });
    } else if (format === "webp") {
      pipeline = pipeline.webp({ quality: 80 });
    } else if (format === "avif") {
      pipeline = pipeline.avif({ quality: 75 });
    } else if (format === "png") {
      pipeline = pipeline.png({ compressionLevel: 9 });
    }

    const buffer = await pipeline.toBuffer();

    // Save to cache
    fs.writeFileSync(cachePath, buffer);

    // Return processed image
    res.set("Content-Type", `image/${format}`);
    res.set("Cache-Control", "public, max-age=31536000");
    res.send(buffer);
  } catch (err) {
    console.error("Image processing error:", err.message);
    res.status(500).json({ error: "Processing failed" });
  }
});

// Health check
app.get("/health", (req, res) => {
  res.json({ status: "ok", uptime: process.uptime() });
});

app.listen(PORT, () => {
  console.log(`Image service running on port ${PORT}`);
});

Run it with Docker Compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
version: "3.8"
services:
  image-service:
    build: .
    container_name: image-service
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      PORT: 3000
      SECRET_KEY: "your-secret-key"
    volumes:
      - ./uploads:/app/uploads
      - ./cache:/app/cache
    deploy:
      resources:
        limits:
          memory: 512M

Usage:

1
2
3
4
5
# Get a 400×300 WebP thumbnail
http://localhost:3000/image/400/300/webp/landscape-photo.jpg

# Get an 800×600 AVIF version
http://localhost:3000/image/800/600/avif/landscape-photo.jpg

Comparison: imgproxy vs Thumbor vs Sharp

FeatureimgproxyThumborSharp (Custom)
LanguageGoPythonNode.js (library)
BackendlibvipsPillow/PILlibvips
Processing speed⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡
Memory usage~50 MB idle~200 MB idleDepends on app
Smart croppingYes (built-in)Yes (with detectors)Manual implementation
Face detectionYesYesNo (external lib needed)
Format support30+ formats15+ formatsAll libvips formats
AVIF support✅ Native✅ With plugin✅ Native
URL-based APICustom implementation
Plugin systemLimitedExtensiveFull code access
CachingBuilt-inResult storage pluginsManual (filesystem/Redis)
Docker image size~80 MB~600 MB~200 MB
Setup complexityLowMediumHigh
Best forSpeed-focused sitesContent platformsCustom app integration

Performance Benchmarks

On a 4-core VPS processing a 4000×3000 JPEG source image to a 800×600 WebP output:

ToolAvg. TimeMemory PeakThroughput
imgproxy12 ms45 MB8,200 req/s (cached)
Thumbor45 ms180 MB2,100 req/s (cached)
Sharp (Express)14 ms60 MB7,500 req/s (cached)

For uncached first-time processing (including source download):

ToolAvg. TimeMemory Peak
imgproxy85 ms90 MB
Thumbor210 ms320 MB
Sharp (Express)95 ms110 MB

Note: Actual performance depends on source image size, transformation complexity, and hardware.

Which Should You Choose?

Choose imgproxy if: You want the fastest possible image processing with minimal setup. It’s ideal for high-traffic websites, e-commerce platforms, and any scenario where raw speed and low resource usage matter most. The URL-based API makes it easy to integrate with any frontend framework — just change your image src URLs.

Choose Thumbor if: You need smart image analysis — face detection, feature-based cropping, and intelligent focal point selection. It’s the best choice for social media platforms, photo galleries, and any application where automatic image composition matters. The plugin ecosystem lets you extend it with custom storage backends, filters, and detection algorithms.

Choose Sharp if: You’re building a Node.js application and want image processing tightly integrated into your codebase. It’s the right pick when you need custom business logic — for example, applying different transformations based on user subscription tiers, generating social media preview images with dynamic text overlays, or batch-processing uploads in a specific workflow.

Production Deployment Checklist

Regardless of which tool you choose, follow these best practices for production:

  1. Always use signed URLs. Prevent abuse by requiring HMAC signatures on image transformation URLs. This stops attackers from generating arbitrary image sizes that could exhaust your server resources.

  2. Set dimension limits. Cap the maximum output dimensions and source image size to prevent denial-of-service attacks through massive image requests.

  3. **Use a CDN in fronnginxPlace Cloudflare, Fastly, or your own Varnish/Nginx cache in front of the image service. Since transformed images are immutable (same URL always returns the same result), CDN caching is extremely effective with hit rates above 95%.

  4. Monitor resource usage. Image processing is CPU-intensive. Set up alerts for CPU usage, memory consumption, and request queue depth. Scale horizontally by running multiple instances behind a load balancer.

  5. Pre-warm popular images. For known high-traffic images (homepage hero, product thumbnails), generate the transformed versions ahead of time during your build or deployment process rather than waiting for the first request.

  6. Serve AVIF to supported browsers. AVIF typically achieves 30–50% smaller file sizes than WebP at equivalent quality. Use content negotiation or <picture> elements to serve AVIF to browsers that support it, falling back to WebP or JPEG.

1
2
3
4
5
<picture>
  <source srcset="/img/unsafe/rs:fit:800:0/plain/photo.jpg@avif" type="image/avif">
  <source srcset="/img/unsafe/rs:fit:800:0/plain/photo.jpg@webp" type="image/webp">
  <img src="/img/unsafe/rs:fit:800:0/plain/photo.jpg@jpeg" alt="Photo" loading="lazy">
</picture>

Self-hosted image optimization is one of the highest-ROI infrastructure investments you can make. A single afternoon of setup pays for itself in faster page loads, lower bandwidth costs, and complete control over your image delivery pipeline.

Frequently Asked Questions (FAQ)

Which one should I choose in 2026?

The best choice depends on your specific requirements:

  • For beginners: Start with the simplest option that covers your core use case
  • For production: Choose the solution with the most active community and documentation
  • For teams: Look for collaboration features and user management
  • For privacy: Prefer fully open-source, self-hosted options with no telemetry

Refer to the comparison table above for detailed feature breakdowns.

Can I migrate between these tools?

Most tools support data import/export. Always:

  1. Backup your current data
  2. Test the migration on a staging environment
  3. Check official migration guides in the documentation

Are there free versions available?

All tools in this guide offer free, open-source editions. Some also provide paid plans with additional features, priority support, or managed hosting.

How do I get started?

  1. Review the comparison table to identify your requirements
  2. Visit the official documentation (links provided above)
  3. Start with a Docker Compose setup for easy testing
  4. Join the community forums for troubleshooting
Advertise here