Back to articles
Technology Insight

Building a Self-Hosted Private Image Optimizer API with Imgproxy on a VPS

May 30, 2026

Introduction: The Cost and Performance Challenge of Modern Web Images

In the modern digital landscape, visual content is paramount to user engagement. However, unoptimized images are often the primary culprit behind sluggish website performance, high bounce rates, and inflated cloud storage and content delivery network (CDN) bills. While commercial services like Cloudinary, Imgix, or AWS CloudFront offer robust image transformation features, their usage-based pricing models can quickly scale out of control as your traffic grows.

For businesses seeking to balance absolute control over infrastructure, predictable budgeting, and high performance, self-hosting is the definitive answer. This guide provides a step-by-step blueprint to building your own Private Image Optimizer API using Imgproxy deployed on a Virtual Private Server (VPS). By the end of this article, you will have a production-ready microservice capable of resizing, cropping, compressing, and converting images on the fly via a highly secure, signature-validated API.

What is Imgproxy and Why Choose It?

Imgproxy is a fast, secure, and lightweight standalone server written in Go that resizes and converts remote images on the fly. It utilizes the highly optimized libvips image processing library rather than the heavier ImageMagick, making it exceptionally fast and incredibly efficient with memory usage.

Key advantages of incorporating Imgproxy into your enterprise architecture include:

  • Extreme Performance: Powered by libvips, it processes images significantly faster than traditional tools while maintaining a minimal RAM footprint.
  • Security-First Architecture: Imgproxy mitigates Denial of Service (DoS) attacks by verifying URL signatures, ensuring that only authorized requests from your applications are processed.
  • Automated Modern Formats: It can automatically detect browser support and serve next-generation formats like WebP and AVIF, reducing payload sizes by up to 80% compared to JPEG.
  • Stateless Nature: Imgproxy does not cache images locally; it acts as a transparent processing layer between your storage (e.g., AWS S3, MinIO, or local directories) and your delivery layer (CDN).

Architecture Overview

Before diving into the technical implementation, it is vital to understand the request lifecycle of a self-hosted image processing pipeline. A standard enterprise-grade deployment follows this structure:

  1. The Client: A user requests an image via a structured URL containing specific dimensions, filters, and a cryptographic signature.
  2. The Reverse Proxy (Nginx/Traefik): Handles SSL termination, rate limiting, and forwards valid traffic to the internal container network.
  3. Imgproxy (Docker): Receives the request, verifies the signature, fetches the original asset from your source storage, performs the transformations in-memory, and streams the output back.
  4. The CDN Layer (Optional but Recommended): Caches the optimized output from Imgproxy to ensure subsequent requests are served instantly from edge locations, minimizing CPU load on your VPS.

Step-By-Step Deployment Guide

Step 1: Preparing Your VPS Environment

To ensure stability and ease of maintenance, we will deploy Imgproxy inside a Docker container. First, connect to your Linux VPS via SSH and update your system packages:

sudo apt update && sudo apt upgrade -y
sudo apt install docker.io docker-compose -y

Verify that Docker is active and running:

sudo systemctl status docker

Step 2: Generating Security Keys

To prevent malicious third parties from using your server to process random images (which drains your server resources), Imgproxy requires URL signature verification. We need to generate a hex-encoded key and salt pair. Execute the following commands in your terminal:

echo $(xxd -g 1 -l 64 -p /dev/random | tr -d ' \n')
echo $(xxd -g 1 -l 64 -p /dev/random | tr -d ' \n')
Important: Save these two long alphanumeric strings safely. They will be configured as IMGPROXY_KEY and IMGPROXY_SALT in your environment configuration. Do not expose them publicly.

Step 3: Configuring Docker Compose

Create a dedicated directory for your image optimization service and navigate into it:

mkdir ~/imgproxy-service && cd ~/imgproxy-service

Create a file named docker-compose.yml using your preferred text editor (e.g., nano) and add the following production-ready configuration:

version: '3.8'

services:
  imgproxy:
    image: darthsim/imgproxy:latest
    container_name: private_imgproxy
    restart: always
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      - IMGPROXY_KEY=your_generated_key_here
      - IMGPROXY_SALT=your_generated_salt_here
      - IMGPROXY_NUM_WORKERS=4
      - IMGPROXY_MAX_SRC_RESOLUTION=50
      - IMGPROXY_ENFORCE_WEBP=true
      - IMGPROXY_ENFORCE_AVIF=true
      - IMGPROXY_ENABLE_WEBP_DETECTION=true
      - IMGPROXY_ENABLE_AVIF_DETECTION=true
      - IMGPROXY_TTL=2592000
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

In this setup, we bind the application port explicitly to 127.0.0.1 to prevent direct public access to port 8080, forcing traffic through our secure reverse proxy. We also enable automated next-gen format detection (AVIF and WebP) based on client request headers.

Step 4: Launching the Microservice

Deploy the service in detached mode by running:

sudo docker-compose up -d

Confirm that the container is healthy and actively listening for connections:

sudo docker ps
curl [http://127.0.0.1:8080/health](http://127.0.0.1:8080/health)

If the configuration is correct, the health endpoint will return a clean OK response.

Securing and Exposing the API via Nginx

To safely expose your new API to your frontend applications, configure Nginx as a reverse proxy accompanied by Let's Encrypt SSL certificates.

Create a new Nginx server block configuration file:

sudo nano /etc/nginx/sites-available/imgproxy.conf

Insert the following configuration, replacing media-api.yourdomain.com with your actual subdomain:

server {
    listen 80;
    server_name media-api.yourdomain.com;

    location / {
        proxy_pass [http://127.0.0.1:8080](http://127.0.0.1:8080);
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Performance buffering tweaks
        proxy_buffering on;
        proxy_buffers 16 16k;
        proxy_buffer_size 32k;
    }
}

Enable the site configuration and reload Nginx to apply changes:

sudo ln -s /etc/nginx/sites-available/imgproxy.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Finally, provision a free SSL certificate via Certbot to ensure all media transit is securely encrypted:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d media-api.yourdomain.com

Generating Secure Signed URLs Programmatically

With signature verification active, Imgproxy will reject raw, unauthenticated requests with a 403 Forbidden error. URLs must follow a strict cryptographic pattern:

[https://media-api.yourdomain.com/](https://media-api.yourdomain.com/){signature}/{processing_options}/{encoded_source_url}.{extension}

Below is an enterprise implementation example using Node.js/TypeScript to programmatically generate valid, signed processing URLs backend-side:

const crypto = require('crypto');

const KEY = 'your_generated_key_here';
const SALT = 'your_generated_salt_here';

function createHexBuffer(hexString) {
  return Buffer.from(hexString, 'hex');
}

function generateSignedImgproxyUrl(sourceUrl, width, height, extension = 'webp') {
  const keyBuffer = createHexBuffer(KEY);
  const saltBuffer = createHexBuffer(SALT);

  // Encode source image URL in URL-safe Base64
  const encodedUrl = Buffer.from(sourceUrl)
    .toString('base64url');

  // Define transformation options (e.g., resize to fit, sharpen)
  const processingOptions = `rs:fill:${width}:${height}:0/sh:0.5`;
  const path = `/${processingOptions}/${encodedUrl}.${extension}`;

  // Create HMAC SHA256 Signature
  const hmac = crypto.createHmac('sha256', keyBuffer);
  hmac.update(saltBuffer);
  hmac.update(Buffer.from(path));
  const signature = hmac.digest('base64url');

  return `https://media-api.yourdomain.com/${signature}${path}`;
}

// Usage Example
const secureUrl = generateSignedImgproxyUrl('[https://your-storage.s3.amazonaws.com/hero.jpg](https://your-storage.s3.amazonaws.com/hero.jpg)', 800, 600);
console.log(secureUrl);

Production Optimization and CDN Caching Strategies

While Imgproxy handles image conversion dynamically at high speed, reprocessing the same asset multiple times for every visitor is an inefficient utilization of your VPS resources. To scale your infrastructure cleanly, implementing a caching layers is essential:

  • Browser Caching: Leverage the IMGPROXY_TTL environment variable to automatically append long-lasting Cache-Control headers to your images, prompting user browsers to store files locally.
  • Edge Caching with a CDN: Place a proxy CDN like Cloudflare or Fastly in front of your subdomain. Configure Cloudflare 'Cache Everything' Page Rules targeting your Imgproxy patterns. This setup guarantees that Imgproxy processes an image variant exactly once; all subsequent requests worldwide are fulfilled directly from CDN caches, reducing your server load down to near-zero.

Conclusion

Building your own private image optimization API using Imgproxy on a VPS provides a highly stable, secure, and infinitely customizable alternative to costly commercial equivalents. By shifting your enterprise architecture to this self-hosted standard, you retain maximum control over data sovereignty, dramatically lower external dependency costs, and ensure your web assets remain fast, responsive, and completely optimized for the end user.

Building a Self-Hosted Private Image Optimizer API with Imgproxy on a VPS | DPTCloud