DEV Community

Aman Sahu
Aman Sahu

Posted on

1 2 1 1 1

How do you serve media files correctly in Docker with Next.js and Nginx!?🛠️🖼️

Spoiler Alert: If you're serving images directly from a CDN like Cloudinary or S3, this post might not be for you; you likely won’t face this issue. But if you're running your own production or development environment with Dockerized Next.js, Django, and Nginx, buckle up, friend. You might just save yourself a few hours of headaches.


🧩 The Problem: Images Playing Hide & Seek

So here's the scene: I’m building a full-stack app using Next.js (frontend), Django (backend), and Nginx (reverse proxy), all running smoothly in their own cosy Docker containers. They even share a common volume for media and static files. Pretty neat, right?

Now, when I upload a profile picture via a Django endpoint, it gets saved in the media/ folder. Because of the shared volume, the image is instantly accessible to Nginx as well. Which means I can hit:

  • http://localhost:8000/media/profile_pics/user.png (via Django)
  • or just http://localhost/media/profile_pics/user.png (via Nginx)

So far, so good.

Then I did something completely logical... I passed that URL directly into the Next.js <Image /> component:

<Image src="http://localhost/media/profile_pics/user.png" alt="User Profile" />
Enter fullscreen mode Exit fullscreen mode

But what did I get? A glorious 500 Internal Server Error. Ta-da.


🔍 So... What Was Going Wrong?

Let’s break it down.

When I passed the image URL to <Image />, Next.js's Image Optimiser internally rewrote the request to something like:

/_next/image?url=http://localhost/media/profile_pics/user.png&w=1920&q=75
Enter fullscreen mode Exit fullscreen mode

The catch? Inside the Next.js container, localhost refers to itself, not to the Nginx container or the host machine. So when Next.js tried to fetch the image from localhost, it hit a dead end because it was looking inside its container, where the image doesn’t exist. It was like asking a cat to bark. Not happening!!


🧪 The "Aha!" Moment (a.k.a. My DIY Proxy Fix)

I could’ve switched to using a CDN... but I wanted to stick with my setup for now. So here's what I did:

The Plan

I built a custom wrapper around the Next.js <Image /> component called <ProxyImage />. This little genius rewrites any "localhost" URLs to use a Docker-resolvable hostname like http://nginx, which points to the right container where the image exists.


🛠️ The Solution (Step-by-Step)

1. Update Your .env

NEXT_PUBLIC_REWRITE_IMAGE_BASES=http://localhost,http://127.0.0.1
NEXT_PUBLIC_IMAGE_REPLACEMENT=http://nginx
Enter fullscreen mode Exit fullscreen mode

This tells the app which base URLs to replace (like http://localhost) and what to replace them with (http://nginx).


2. Update Your config.ts

const config = {
  imageRewrite: {
    baseUrls: (process.env.NEXT_PUBLIC_REWRITE_IMAGE_BASES || "").split(","),
    replacement: process.env.NEXT_PUBLIC_IMAGE_REPLACEMENT || "",
  },
};
Enter fullscreen mode Exit fullscreen mode

Let’s keep things dynamic and DRY, shall we?


3. Build the ProxyImage Component

import Image, { ImageProps } from 'next/image';
import config from '@/config';

const { baseUrls, replacement } = config.imageRewrite;

function rewriteSrc(src: string): string {
  for (const base of baseUrls) {
    if (src.startsWith(base)) {
      return src.replace(base, replacement);
    }
  }
  return src;
}

interface ProxyImageProps extends Omit<ImageProps, 'src'> {
  src: string;
}

export const ProxyImage = ({ src, ...props }: ProxyImageProps) => {
  const fixedSrc = rewriteSrc(src);
  return <Image {...props} src={fixedSrc} />;
};
Enter fullscreen mode Exit fullscreen mode

Now just use <ProxyImage /> wherever you need optimized images.


4. Allow External Image Domains in next.config.js

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'http',
        hostname: 'nginx',
        pathname: '/media/**',
      },
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode

This tells Next.js: “Hey, it’s cool to optimise images from http://nginx.”


✅ Why This Worked For Me

  • Docker-Friendly: Rewrites URLs to something your container can resolve.
  • Leverages Next.js Optimisation: You still get all the goodies like lazy loading, resizing, etc.
  • Flexible Config: URLs and replacement targets are environment-based.
  • No Extra Services Needed: No need to spin up a CDN just for local dev.

✨ Bonus Thoughts

There are other ways to solve this problem, like skipping optimisation altogether for internal images, preloading them, or routing through an API, but this setup felt like the most robust and flexible option for my Dockerized local stack.

If you’ve got a better solution, I’d love to hear it! You do you, dev.


🚀 TL;DR

  • If your images are served from localhost in Docker, Next.js's Image component might fail.
  • The fix? Create a wrapper that rewrites localhost URLs to a valid Docker hostname like nginx.
  • Your images load. Your errors vanish. Your dev life becomes 5% happier.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Discover more in this insightful article and become part of the thriving DEV Community. Developers at every level are welcome to share and enrich our collective expertise.

A simple “thank you” can brighten someone’s day. Please leave your appreciation in the comments!

On DEV, sharing skills lights our way and strengthens our connections. Loved the read? A quick note of thanks to the author makes a real difference.

Count me in