devops2024-11-25·9 min·249/348

Configuring next/image External Image Domain Allowlists

How to properly configure external image domains and patterns in next/image for security and performance

Configuring next/image External Image Domain Allowlists

Introduction

Next.js restricts which external image domains can be used with the next/image component. This is a security feature that prevents your application from being used as an open proxy for image optimization. However, configuring the allowlist incorrectly leads to runtime errors and broken images.

This post explains the two approaches to allowing external image domains, their differences, and how to configure them correctly.

Environment

  • OS: Windows 11
  • Node.js: v20.10.0
  • Next.js: 14.0.4
  • External Image Sources: Unsplash, Cloudinary, S3-compatible storage

Problem

Using an unconfigured external image produces:

Error: Invalid src prop (https://images.unsplash.com/photo-123456) on `next/image`,
hostname "images.unsplash.com" is not configured under images in next.config.js

Even after adding the domain, images may still fail:

Error: Invalid remote config for image optimization
The "hostname" option is deprecated. Use "remotePatterns" instead.
Error: Image optimization is not supported for the "http" protocol.
Use "https" instead.

Analysis

Next.js provides two methods for configuring external image domains:

1. images.domains (deprecated). A simple array of hostnames. Works but lacks security features like pathname filtering and protocol restriction.

2. images.remotePatterns (recommended). A structured configuration that allows fine-grained control over which URLs are permitted.

The deprecated domains option does not support:

  • Restricting by pathname
  • Restricting by protocol (HTTP vs HTTPS)
  • Wildcard subdomain matching with pattern syntax

Solution

Fix 1: Use remotePatterns (recommended)

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      // Unsplash - allow all images
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
      // Cloudinary - allow specific account
      {
        protocol: 'https',
        hostname: '*.cloudinary.com',
        pathname: '/demo/**',
      },
      // S3 bucket - restrict to specific path
      {
        protocol: 'https',
        hostname: 'my-bucket.s3.amazonaws.com',
        pathname: '/images/**',
      },
      // localhost for development
      {
        protocol: 'http',
        hostname: 'localhost',
        port: '3000',
      },
    ],
  },
}

module.exports = nextConfig

Fix 2: Handle CDN and proxy configurations

// next.config.js
const nextConfig = {
  images: {
    remotePatterns: [
      // Contentful CDN
      {
        protocol: 'https',
        hostname: 'images.ctfassets.net',
        pathname: '/**',
      },
      // WordPress media
      {
        protocol: 'https',
        hostname: '*.wp.com',
        pathname: '/**',
      },
      // Custom CDN with subdomains
      {
        protocol: 'https',
        hostname: '*.cdn.example.com',
        pathname: '/media/**',
      },
    ],
    // Set minimum cache TTL for external images
    minimumCacheTTL: 60 * 60 * 24, // 24 hours
    // Define device sizes for responsive images
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
  },
}

module.exports = nextConfig

Fix 3: Environment-based configuration

// next.config.js
const isDev = process.env.NODE_ENV === 'development'

const devPatterns = [
  {
    protocol: 'http',
    hostname: 'localhost',
  },
]

const prodPatterns = [
  {
    protocol: 'https',
    hostname: 'images.unsplash.com',
  },
  {
    protocol: 'https',
    hostname: '*.cloudinary.com',
    pathname: '/my-app/**',
  },
]

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: isDev ? devPatterns : prodPatterns,
  },
}

module.exports = nextConfig

Fix 4: Use the image component correctly with allowed domains

// components/ExternalImage.tsx
import Image from 'next/image'

interface ExternalImageProps {
  src: string
  alt: string
  width?: number
  height?: number
}

export function ExternalImage({
  src,
  alt,
  width = 800,
  height = 600,
}: ExternalImageProps) {
  // Validate that the image URL is from an allowed domain
  const allowedHosts = [
    'images.unsplash.com',
    'images.ctfassets.net',
  ]

  const url = new URL(src)
  if (!allowedHosts.includes(url.hostname)) {
    console.warn(`Image from ${url.hostname} is not in the allowlist`)
    return null
  }

  return (
    {alt}
  )
}

Lessons Learned

  1. Always use remotePatterns over domains. The domains option is deprecated and will be removed in a future version.

  2. Be as specific as possible. Restricting by hostname and pathname reduces the attack surface if your application is compromised.

  3. Use http protocol only for localhost. Production external images should always use https.

  4. Test with actual image URLs from each domain. A typo in the hostname configuration will not produce a build error but will cause runtime failures.

  5. Monitor image optimization bandwidth. Each optimized image consumes server resources. Use minimumCacheTTL to prevent re-optimizing the same image repeatedly.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.