Back to articles
Technology Insight

Building a Self-Hosted Private Image Optimization CDN using Imgproxy and Cloudflare on a VPS

June 3, 2026

Introduction: The Cost and Performance Dilemma of Modern Web Assets

In the modern digital landscape, visual content dictates user engagement and conversion rates. However, unoptimized high-resolution media is also the single largest contributor to page bloat, leading to slow loading speeds and diminished Search Engine Optimization (SEO) rankings. Traditionally, engineering teams rely on third-party Image CDNs to perform dynamic, on-the-fly resizing, cropping, and format conversion. While effective, enterprise SaaS solutions rapidly escalate in cost as application traffic scales.

For enterprises seeking full technical data sovereignty and strict cost control, building a Self-Hosted Private Image Optimization CDN is the ideal paradigm shift. This guide provides a comprehensive blueprint to architecting a reliable, production-ready image delivery infrastructure. We will deploy Imgproxy—a hyper-fast, memory-safe standalone server written in Go—on an isolated Virtual Private Server (VPS), while utilizing Cloudflare as an intelligent proxy, security shield, and global edge caching layer to achieve near-zero latency.

---

1. Architectural Overview: How the Self-Hosted CDN Operates

Before executing the deployment, it is vital to understand the multi-tiered pipeline that requests traverse. Rather than overloading your primary application database or storage servers with pre-rendered thumbnails, our architecture employs on-the-fly execution with edge caching.

The sequence of operations occurs as follows:

  1. Client Request: A user requests an optimized image variant via a structured URL (e.g., specifying width, height, and format).
  2. Cloudflare Edge Lookup: Cloudflare intercepts the request. If the requested variation resides in the edge cache, it is immediately served to the user, bypassing your origin server entirely.
  3. VPS Routing & Validation: On a cache miss, Cloudflare forwards the request to your VPS. A reverse proxy (such as Nginx) accepts the traffic, enforces cryptographic signature checks to prevent URL tampering, and routes the request to Imgproxy.
  4. On-the-Fly Processing: Imgproxy fetches the original source asset from your private storage or application server, processes it in-memory (resizing, compressing, and converting to next-generation formats like WebP or AVIF), and streams it back.
  5. Caching and Delivery: Cloudflare caches the processed result at the edge for subsequent visitors, ensuring your VPS compute is heavily shielded from repetitive processing tasks.
---

2. Server Provisioning and Initial Environment Setup

To ensure high throughput and low overhead, we utilize Docker to containerize our image processing microservices. Begin by provisioning a standard Linux-based VPS (Ubuntu 24.04 LTS or similar) and executing system updates.

### Step 2.1: System Preparation and Engine Installation

Connect to your VPS via SSH and install the Docker engine and Docker Compose core plugins:

sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y docker.io docker-compose-plugin ufw
### Step 2.2: Firewall Configuration

To ensure that only Cloudflare can communicate with your origin processing server, configure the Uncomplicated Firewall (UFW) to lock down public access, restricting incoming traffic on web ports exclusively to authenticated reverse proxies or Cloudflare IP ranges:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
---

3. Implementing Imgproxy with Secure Containerization

Imgproxy stands out because it does not store images on disk; it acts as a stateless processor. Security is highly critical here: if your endpoint is publicly exposed without signing keys, malicious actors could perform a Denial of Service (DoS) attack by requesting infinite variations of images (e.g., changing dimensions by 1 pixel continuously) to exhaust your server's CPU.

### Step 3.1: Generating Cryptographic Keys

We will generate a random hex-encoded Key and Salt pair. These variables are mandatory for signing image URLs securely.

echo $(xxd -g 2 -l 64 /dev/urandom | awk '{print $2$3$4$5$6$7$8$9}' | tr -d '
')

Note: Generate two separate strings—one for IMGPROXY_KEY and one for IMGPROXY_SALT. Keep these strings strictly confidential.

### Step 3.2: Writing the Docker Compose Manifest

Create a dedicated directory and construct a docker-compose.yml file to manage the Imgproxy container and its configuration variables.

version: '3.8'

services:
  imgproxy:
    image: darthsim/imgproxy:latest
    container_name: private_imgproxy
    restart: always
    environment:
      - IMGPROXY_BIND=0.0.0.0:8080
      - IMGPROXY_KEY=your_generated_hex_key
      - IMGPROXY_SALT=your_generated_hex_salt
      - IMGPROXY_MAX_SRC_RESOLUTION=50
      - IMGPROXY_AUTO_WEBP_ACCEPT=true
      - IMGPROXY_AUTO_AVIF_ACCEPT=true
      - IMGPROXY_TTL=2592000
    ports:
      - "127.0.0.1:8080:8080"

In this production-hardened configuration, we enforce IMGPROXY_AUTO_WEBP_ACCEPT and IMGPROXY_AUTO_AVIF_ACCEPT. This instructs Imgproxy to automatically analyze the user's browser HTTP Accept header and seamlessly deliver WebP or AVIF formats without changing the extension in the URL if the client's browser supports it.

---

4. Configuring Nginx as a Secure Local Reverse Proxy

While Imgproxy is running locally on port 8080, we use Nginx to manage upstream communication, handle SSL handshakes, and format upstream HTTP cache-control responses effectively.

### Step 4.1: Upstream Proxy Block

Create an Nginx server block configuration pointing to your dedicated optimization domain (e.g., media.yourcompany.com):

server {
    listen 80;
    server_name media.yourcompany.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;

        # Standardize long-term caching headers for Cloudflare overrides
        add_header Cache-Control "public, max-age=31536000, immutable";
    }
}
---

5. Integrating Cloudflare for Global Caching and Edge Security

With your VPS fully configured, Cloudflare acts as the global distribution mechanism, minimizing the resource footprint on your physical server.

### Step 5.1: DNS and Proxy Settings

Navigate to your Cloudflare Dashboard and point an A Record for your media subdomain (e.g., media) directly to your VPS public IPv4 address. Crucially, ensure that the Proxy Status toggle is set to Proxied (Orange Cloud).

### Step 5.2: Enforcing Edge Cache via Cache Rules

To prevent Cloudflare from bypassing the cache on complex query parameters or specific image request types, you must implement a strict Cache Rule within the Cloudflare dashboard:

  • Navigate to Caching > Cache Rules and create a new rule.
  • Set the matching criteria to: Hostname equals media.yourcompany.com.
  • Under Cache Eligibility, select Eligible for cache.
  • In the advanced configuration, enforce Edge TTL to a long duration, such as 1 month. This ensures that once Imgproxy processes an asset variant, Cloudflare keeps it at the regional edge locations indefinitely, resulting in subsequent sub-millisecond response times.
---

6. Programmatic URL Generation and Production Validation

Because security is enforced via IMGPROXY_KEY and IMGPROXY_SALT, your backend application must sign URLs before serving them to users. Unsigned paths will be rejected with an HTTP 403 Forbidden error.

A production-ready request structure follows this precise pattern:

[https://media.yourcompany.com/](https://media.yourcompany.com/)%{signature}/resize:%{resizing_type}:%{width}:%{height}:%{enlarge}/plain/%{source_image_url}

Below is a standardized implementation example using Node.js/TypeScript to generate authenticated, optimized production asset URLs automatically:

import * as crypto from 'crypto';

function generateSignedImgproxyUrl(
    sourceUrl: string,
    width: number,
    height: number
): string {
    const key = Buffer.from("your_generated_hex_key", 'hex');
    const salt = Buffer.from("your_generated_hex_salt", 'hex');
    
    const encodedUrl = Buffer.from(sourceUrl).toString('base64url');
    const path = `/resize:fill:${width}:${height}:0/plain/${encodedUrl}`;
    
    const hmac = crypto.createHmac('sha256', key);
    hmac.update(salt);
    hmac.update(Buffer.from(path));
    const signature = hmac.digest('base64url');
    
    return `https://media.yourcompany.com/${signature}${path}`;
}
---

Conclusion: Total Freedom Over Visual Infrastructure

By bypassing third-party managed Image CDNs and assembling your own stack utilizing Imgproxy and Cloudflare on an affordable VPS, you unlock a highly resilient infrastructure capable of handling millions of dynamic media requests. This self-hosted setup guarantees that storage costs remain static, page performance scales efficiently, and your technical ecosystem remains highly optimized, cost-predictable, and entirely under your corporate administration.

Building a Self-Hosted Private Image Optimization CDN using Imgproxy and Cloudflare on a VPS | DPTCloud