MailSlurp logo

blog

NextJS custom image loader self hosted Thumbor

How to optimize next.js Image tag without paying for Vercel or other cloud image services

When building modern web applications, images play a crucial role in the user experience. Optimizing these images for different devices, screen sizes, and network conditions is essential for performance, especially with Next.js, which includes a built-in image component that attempts to solve the problem of handling responsive images. However, using paid cloud services for image optimization, like Vercel or Cloudinary, might not be the best option for everyone.

In this blog post, we'll explore why image optimization in Next.js is challenging, discuss popular cloud-based solutions (and their drawbacks), introduce Thumbor for self-hosted image optimization, and guide you through deploying it with NGINX as a cache layer. We'll also show how to use the Next.js Image component with Thumbor.

Why Image Optimization in Next.js is Hard

Next.js provides the Image component to handle various complexities of responsive images, such as lazy loading, automatic resizing, and serving different image formats like WebP. This feature is awesome for performance, but it comes with its challenges:

// import Image from 'next/image'
<Image src={'Pain and suffering'} alt={'Or is it?'} />

Key Challenges:

  • Custom Image Loaders: Next.js's default image loader works seamlessly with Vercel (if you're hosting on Vercel). But what if you host elsewhere? You'll need to use custom image loaders and configure them properly for image optimization.
  • Third-party Dependence: While using services like Vercel's built-in optimization or Cloudinary seems convenient, you're relying on a third-party service, which can introduce costs, vendor lock-in, and limited control.
  • Scaling for Global Users: Optimizing images for users across different regions can be tough. You need to handle caching and deliver images quickly regardless of location.
  • Custom Storage: If you store images in S3 or another cloud service, you may need to figure out how to serve these optimized images without excessive network requests or performance degradation.

These challenges make it clear that image optimization is not a one-size-fits-all problem.

Have you tried shouting?

Current Options (and Why They Suck)

Most developers default to using services like Vercel, Cloudinary, or Imgix for image optimization. While they are popular and seem like convenient solutions, there are some major drawbacks.

Vercel Image Optimization

  • Pros: Fully integrated with Next.js, automatic responsive images, great for small to medium apps.
  • Cons: It's paid! And not cheap. Additionally, you're tied to Vercel's hosting and usage limits. If you scale out and need custom image hosting (like S3), it becomes tricky.

Cloudinary

  • Pros: Cloudinary supports various image formats, transformations, and an easy-to-use API.
  • Cons: Expensive pricing for bandwidth, transformations, and storage. Vendor lock-in is a huge issue here-once you're in, migrating out is complex and painful.

Imgix

  • Pros: Provides powerful transformation tools and global image delivery.
  • Cons: Same as Cloudinary: cost, vendor lock-in, and complexity. Their pricing models are hard to predict if your app grows.

These services add convenience, but at the cost of control and sometimes unpredictable scaling costs. For developers looking for more control over their infrastructure, self-hosting becomes an attractive option. This is where Thumbor comes in.

What Thumbor Is and How It Can Optimize Images from S3 and Cache Behind NGINX

Thumbor is an open-source smart imaging service that enables you to resize, optimize, and cache images on the fly. It's lightweight, flexible, and a great choice for developers who want to handle image optimization without relying on expensive external services.

Key Features of Thumbor:

  • On-the-fly Image Resizing: Just specify the desired dimensions in the URL, and Thumbor will handle the resizing.
  • Smart Cropping: Thumbor can automatically detect the important parts of the image to focus on when cropping.
  • Image Format Conversion: It can convert images into WebP for browsers that support it, automatically reducing file size.
  • Caching: Thumbor can be easily integrated with NGINX to cache the optimized images, reducing the load on your server and speeding up delivery.
  • S3 Integration: Thumbor can fetch images directly from S3 and optimize them before serving them to users.

Why Thumbor Over Cloud Services?

  • Cost Efficiency: You host it yourself, no more vendor fees.
  • Complete Control: You decide the storage backend (like S3) and caching strategy (like NGINX).
  • Scalability: Thumbor can be scaled horizontally to serve thousands of images if needed.

Thumbor + NGINX Caching

By integrating NGINX with Thumbor, you can cache the images locally. This means once Thumbor optimizes an image, NGINX can serve the cached version to reduce redundant processing.

How to Deploy your own image optimization with NGINX Caching on Kubernetes

To deploy Thumbor and NGINX on Kubernetes, we will use a combination of a Kubernetes Deployment and an NGINX sidecar container. Additionally, we'll set up an AWS CLI sidecar to sync images from S3 to a local directory. This method allows you to fetch images from S3, optimize them using Thumbor, and cache the optimized images with NGINX.

Here's how you can set it up using a Kubernetes YAML Deployment.

Step 1: Kubernetes Deployment YAML for Thumbor, NGINX, and S3 Sync

apiVersion: apps/v1
kind: Deployment
metadata:
  name: thumbor
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: thumbor
  template:
    metadata:
      labels:
        app: thumbor
    spec:
      containers:
        # NGINX container to serve as a caching proxy
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80 # NGINX listens on port 80
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf # Load custom NGINX configuration

        # Thumbor container to handle image optimization
        - name: thumbor
          image: apsl/thumbor:latest
          ports:
            - containerPort: 8000 # Thumbor listens on port 8000
          env:
            - name: LOADER
              value: "thumbor.loaders.file_loader"
            - name: FILE_LOADER_ROOT_PATH
              value: "/data/images" # Local path where images are stored
            - name: RESULT_STORAGE
              value: "thumbor.result_storages.file_storage" # Cache processed images
            - name: RESULT_STORAGE_FILE_STORAGE_ROOT_PATH
              value: "/data/cache"
          volumeMounts:
            - name: images
              mountPath: /data/images # Mount images directory
            - name: cache
              mountPath: /data/cache # Mount cache directory

        # AWS CLI sidecar to sync images from S3
        - name: s3-sync
          image: amazon/aws-cli:latest
          command: ["/bin/sh", "-c"]
          args:
            - |
              while true; do
                aws s3 sync s3://your-s3-bucket-name/images /data/images;
                sleep 600;  # Sync every 10 minutes
              done;
          volumeMounts:
            - name: images
              mountPath: /data/images # Sync images to the Thumbor images directory

      volumes:
        - name: nginx-config
          configMap:
            name: nginx-config # Load NGINX config from a ConfigMap
        - name: images
          emptyDir: {} # Temp storage for images synced from S3
        - name: cache
          emptyDir: {} # Temp storage for cached images

Key Parts of the Deployment:

  • Thumbor container:
  • Uses the file loader to load images from the /data/images directory, where the S3 images are synced.
  • Stores the optimized and transformed images in the /data/cache directory for quick access on subsequent requests.
  • NGINX container:
  • Acts as a reverse proxy and caching layer for Thumbor. Requests are cached in NGINX after being processed by Thumbor, reducing the load on the Thumbor service for repeated image requests.
  • AWS CLI Sidecar:
  • Syncs images from S3 every 10 minutes to the /data/images directory, where Thumbor picks them up for processing.

Step 2: ConfigMap for NGINX Configuration

Here's an example ConfigMap for NGINX that proxies requests to Thumbor and caches them:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  namespace: default
data:
  nginx.conf: |
    user  nginx;
    worker_processes  auto;
    error_log  /var/log/nginx/error.log warn;
    pid        /var/run/nginx.pid;

    events {
      worker_connections  1024;
    }

    http {
      include       /etc/nginx/mime.types;
      default_type  application/octet-stream;
      sendfile        on;
      keepalive_timeout  65;

      # Enable caching for NGINX
      proxy_cache_path  /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;

      server {
        listen 80;

        # Health check endpoint for "/"
        location = / {
          return 200 'OK';
          add_header Content-Type text/plain;
        }

        # Proxy all other requests to Thumbor
        location / {
          proxy_pass http://localhost:8000;  # Thumbor is running on port 8000 inside the same pod
          proxy_cache my_cache;  # Cache responses from Thumbor
          proxy_cache_valid 200 302 10m;  # Cache 200 and 302 responses for 10 minutes
          proxy_cache_valid 404 1m;  # Cache 404 responses for 1 minute
          proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
          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;
        }
      }
    }

Step 3: Use Next.js with the Thumbor Deployment

Now that Thumbor is up and running on Kubernetes, you can configure the Next.js Image component to use Thumbor as its custom image loader. This will allow Next.js to request optimized images from Thumbor.

Here's an example of how to set up the Next.js Image Component with the custom loader:

const thumborLoader = ({ src, width, quality }) => {
  const baseUrl = "https://your-nginx-server-url.com"; // Replace with your NGINX/Thumbor server URL
  const qualityParam = quality ? quality : 75; // Default quality
  return `${baseUrl}/unsafe/fit-in/${width}x0/${src}?quality=${qualityParam}`;
};

export default function MyImageComponent(props) {
  return (
    <Image
      loader={thumborLoader}
      src={props.src}
      width={props.width}
      height={props.height} // Optional, can rely on width to auto-calculate height
      alt={props.alt}
      quality={props.quality} // Optional, controls quality of the image
    />
  );
}

Step 4: Deploy and Test

Deploy the Kubernetes YAML configuration and ensure that:

  • Images are synced from S3 to Thumbor using the AWS CLI sidecar.
  • NGINX caches optimized images and proxies requests to Thumbor.
  • Next.js uses the Thumbor URL for all images, delivering optimized versions based on the requested width.

Recap of How It Works:

  1. Thumbor is responsible for dynamically resizing and optimizing images.
  2. NGINX serves as a caching layer to reduce redundant Thumbor requests.
  3. AWS CLI sidecar syncs images from S3 to the Thumbor pod's local filesystem.
  4. Next.js Image Component uses Thumbor for image optimization, requesting different sizes and qualities depending on the device and network.

By self-hosting Thumbor and integrating it with NGINX for caching and S3 for storage, you gain full control over your image optimization pipeline. Plus, it's highly scalable and cost-effective compared to third-party services like Cloudinary or Vercel's image optimization.

Why Self-hosting Image Optimization is Powerful

While cloud services like Vercel, Cloudinary, and Imgix provide convenience, self-hosting with tools like Thumbor gives you:

  • Full Control: You decide where and how your images are stored, processed, and served.
  • Scalability: You can scale Thumbor instances as your application grows without worrying about third-party limits.
  • Cost-Effectiveness: Instead of paying for every image transformation or bandwidth with Cloudinary, you control the infrastructure and minimize recurring costs.

Final Thoughts

By self-hosting Thumbor, you're free from vendor lock-in and high fees while retaining full control over how your images are optimized and served. With tools like NGINX for caching and S3 for storage, your Next.js app can serve optimized images just as efficiently as with paid cloud services, without the costs.