Image Transformations for Self-Hosted Supabase: Complete imgproxy Guide

Set up imgproxy for self-hosted Supabase to resize, optimize, and serve images on the fly. Includes configuration, security, and performance tips.

Cover Image for Image Transformations for Self-Hosted Supabase: Complete imgproxy Guide

When you self-host Supabase, image transformations aren't just a nice-to-have—they're essential for building performant applications. Serving a 4MB JPEG when a 100KB WebP would do wastes bandwidth, slows down your app, and frustrates users on mobile connections.

Supabase Cloud includes image transformations out of the box. Self-hosters need to configure imgproxy themselves. This guide walks through the complete setup: from deploying the container to production optimization and troubleshooting common issues.

Why Image Transformations Matter

Before diving into configuration, let's understand what we're solving.

The problem: Users upload images in whatever format and resolution their devices produce. Modern phones capture 48MP images. A product catalog page displaying 20 products doesn't need 20 full-resolution images—it needs thumbnails optimized for the user's screen.

The solution: Transform images on-the-fly. When a client requests an image, resize it, convert it to an efficient format (WebP, AVIF), and cache the result. Subsequent requests serve the cached version.

Supabase uses imgproxy under the hood—a fast, secure image processing server written in Go. It's battle-tested and handles millions of transformations daily across Supabase's infrastructure.

Basic imgproxy Setup

The Supabase Docker Compose stack includes imgproxy by default, but it requires explicit configuration to enable image transformations.

Step 1: Verify imgproxy Is Running

First, check your Docker Compose file includes the imgproxy service:

imgproxy:
  image: darthsim/imgproxy:latest
  container_name: supabase-imgproxy
  restart: unless-stopped
  environment:
    - IMGPROXY_BIND=:8080
    - IMGPROXY_LOCAL_FILESYSTEM_ROOT=/
    - IMGPROXY_USE_ETAG=true
    - IMGPROXY_ENABLE_WEBP_DETECTION=true
  volumes:
    - ./volumes/storage:/var/lib/storage
  networks:
    - supabase-network

Verify it's running:

docker ps | grep imgproxy

Step 2: Configure Storage API

The Storage API service needs to know where imgproxy lives. Add these environment variables to your storage service:

ENABLE_IMAGE_TRANSFORMATION=true
IMGPROXY_URL=http://imgproxy:8080

In your docker-compose.yml:

storage:
  image: supabase/storage-api:latest
  environment:
    # ... existing config ...
    ENABLE_IMAGE_TRANSFORMATION: "true"
    IMGPROXY_URL: "http://imgproxy:8080"

Restart both services:

docker compose restart storage imgproxy

Step 3: Test the Setup

Upload a test image to any bucket, then request it with transformation parameters:

# Original image
curl "https://your-supabase-url/storage/v1/object/public/bucket/test.jpg"

# Resized to 200px width
curl "https://your-supabase-url/storage/v1/render/image/public/bucket/test.jpg?width=200"

If you get a transformed image, the basic setup is working. If not, check the Storage API logs:

docker logs supabase-storage-api 2>&1 | grep -i imgproxy

Essential Configuration Options

The default imgproxy configuration works, but production deployments need tuning. Here are the environment variables that matter.

Image Resolution Limits

By default, imgproxy limits source images to 16.8 megapixels. Modern phone cameras easily exceed this. Increase the limit:

environment:
  - IMGPROXY_MAX_SRC_RESOLUTION=50  # 50 megapixels

The Supabase docs claim 50MP support, but this requires explicit configuration. Without it, users uploading high-resolution photos will get errors.

Output Quality

Balance quality against file size:

environment:
  - IMGPROXY_QUALITY=80           # JPEG quality (1-100)
  - IMGPROXY_AVIF_SPEED=5         # AVIF encoding speed (0=slowest, 8=fastest)
  - IMGPROXY_PNG_QUANTIZATION_COLORS=256  # PNG color palette size

Quality 80 is a reasonable default—visually indistinguishable from 100 for most images, but significantly smaller files.

Memory Management

imgproxy uses memory proportional to image dimensions, not file size. A 4000x3000 pixel image uses the same memory whether it's a 200KB JPEG or a 20MB TIFF.

environment:
  - IMGPROXY_MAX_ANIMATION_FRAMES=100  # Limit GIF frames processed
  - IMGPROXY_DOWNLOAD_BUFFER_SIZE=0    # Stream instead of buffering

For containers with limited memory:

imgproxy:
  deploy:
    resources:
      limits:
        memory: 1G
      reservations:
        memory: 256M

Caching Configuration

imgproxy generates ETag headers by default, enabling client-side caching. For server-side caching, consider adding a reverse proxy cache:

environment:
  - IMGPROXY_USE_ETAG=true
  - IMGPROXY_CACHE_CONTROL_PASSTHROUGH=true

If you're using Nginx or Traefik as a reverse proxy, configure caching there for transformed images.

Security Hardening

imgproxy should never be directly exposed to the internet. The Storage API proxies requests and handles authentication.

Network Isolation

Ensure imgproxy is only accessible from the Storage API:

imgproxy:
  networks:
    - supabase-internal
  # No port mapping - not exposed to host

Signed URLs

When using private buckets, the Storage API validates JWTs before forwarding to imgproxy. This prevents unauthorized image access while still allowing transformations.

const { data } = await supabase.storage
  .from('private-bucket')
  .createSignedUrl('image.jpg', 3600, {
    transform: {
      width: 200,
      height: 200,
      resize: 'cover'
    }
  });

Rate Limiting

Add rate limiting at the reverse proxy level to prevent abuse:

# Nginx example
limit_req_zone $binary_remote_addr zone=imgproxy:10m rate=10r/s;

location /storage/v1/render/ {
    limit_req zone=imgproxy burst=20 nodelay;
    proxy_pass http://supabase-storage:5000;
}

Using Transformations in Your App

Supabase provides two ways to request transformed images.

Public Buckets

For public buckets, use the render endpoint:

const { data: { publicUrl } } = supabase.storage
  .from('public-bucket')
  .getPublicUrl('image.jpg', {
    transform: {
      width: 400,
      height: 300,
      resize: 'contain'  // or 'cover', 'fill'
    }
  });

Private Buckets with Signed URLs

For private buckets, include transformations when creating signed URLs:

const { data } = await supabase.storage
  .from('private-bucket')
  .createSignedUrl('image.jpg', 3600, {
    transform: {
      width: 400,
      quality: 75,
      format: 'webp'
    }
  });

Available Transform Options

ParameterValuesDescription
width1-2500Output width in pixels
height1-2500Output height in pixels
resizecover, contain, fillResize mode
quality20-100Output quality (default: 80)
formatwebp, avif, originOutput format

Format detection: When you don't specify a format, Supabase automatically serves WebP to browsers that support it. This reduces bandwidth without any code changes.

Performance Optimization

CDN Integration

Transformed images are perfect for CDN caching. Each URL with specific transform parameters produces a deterministic result:

/storage/v1/render/image/public/bucket/photo.jpg?width=200&quality=80

Put a CDN like Cloudflare or Bunny in front of your Supabase instance. The first request transforms the image; subsequent requests serve from CDN cache.

Pregenerate Common Sizes

For frequently accessed images (profile photos, product thumbnails), consider pregenerating common sizes during upload using Edge Functions:

import { createClient } from '@supabase/supabase-js';

const sizes = [100, 200, 400, 800];

Deno.serve(async (req) => {
  const { bucket, path } = await req.json();
  const supabase = createClient(/* ... */);
  
  // Warm the cache by requesting each size
  for (const width of sizes) {
    const url = supabase.storage
      .from(bucket)
      .getPublicUrl(path, { transform: { width } });
    
    await fetch(url.data.publicUrl);
  }
  
  return new Response('OK');
});

Monitor Memory Usage

High-resolution images can spike memory usage. Monitor imgproxy container metrics:

docker stats supabase-imgproxy

If you see memory spikes causing OOM kills, either:

  1. Increase container memory limits
  2. Reduce IMGPROXY_MAX_SRC_RESOLUTION
  3. Add IMGPROXY_CONCURRENCY to limit parallel processing

Troubleshooting Common Issues

"Source image resolution is too big"

Cause: Image exceeds IMGPROXY_MAX_SRC_RESOLUTION (default 16.8MP).

Fix: Increase the limit in imgproxy environment:

- IMGPROXY_MAX_SRC_RESOLUTION=50

Transformations Return 404

Cause: Storage API can't reach imgproxy.

Check:

  1. Both services on same Docker network?
  2. IMGPROXY_URL correctly configured in Storage API?
  3. imgproxy container running?
docker exec supabase-storage-api curl -v http://imgproxy:8080/health

Slow Transformation Times

Cause: Large images, complex operations, or resource constraints.

Fix:

  1. Resize before upload when possible
  2. Increase imgproxy container resources
  3. Add caching layer (CDN or reverse proxy cache)

Format Not Converting

Cause: Client browser doesn't advertise WebP/AVIF support.

Check: Ensure IMGPROXY_ENABLE_WEBP_DETECTION=true is set.

When Not to Use On-the-Fly Transformations

Real-time transformations add latency (typically 50-200ms for the first request). Consider alternatives when:

  • Serving static assets: Pregenerate during build time
  • Extremely high traffic: Pregenerate and serve from CDN
  • Complex operations: Use a dedicated image pipeline

For most applications, on-the-fly transformation with CDN caching provides the best balance of flexibility and performance.

What Supascale Handles

Setting up imgproxy correctly involves Docker configuration, network isolation, security hardening, and performance tuning. Supascale automates this infrastructure:

  • imgproxy comes preconfigured with production-ready settings
  • Network isolation handled automatically
  • Automated backups include Storage files
  • Custom domains work seamlessly with image URLs

For teams that want image transformations working out of the box without the infrastructure overhead, check out our pricing for a one-time purchase that includes unlimited projects.

Further Reading