---
title: "Serving assets and images in Next.js from a CDN without Vercel"
description:
  "Serve assets and public images in Next.js app from a CDN without deploying on
  Vercel"
canonical_url: "https://www.bigbinary.com/blog/serving-nextjs-assets-images-cdn"
markdown_url: "https://www.bigbinary.com/blog/serving-nextjs-assets-images-cdn.md"
---

# Serving assets and images in Next.js from a CDN without Vercel

Serve assets and public images in Next.js app from a CDN without deploying on
Vercel

- Author: Akash Srivastava
- Published: November 28, 2023
- Categories: Next.js

[NeetoCourse](https://neetocourse.com) allows you to build and sell courses
online. [BigBinary Academy](https://courses.bigbinaryacademy.com/) runs on
NeetoCourse. NeetoCourse uses Next.js to build its pages.

If one uses Vercel to deploy a Next.js application, Vercel
[automatically configures a global CDN](https://nextjs.org/docs/app/building-your-application/deploying#managed-nextjs-with-vercel).
However, NeetoCourse is hosted at
[NeetoDeploy](https://neetodeploy.com/neetodeploy), our own app deployment
platform. This meant we had to figure out how to serve assets and images from a
CDN. We like using Cloudflare for various things, and we decided to use
Cloudflare as the CDN for this case.

There are two kinds of files in a server-side rendered Next.js app that can be
served from a CDN.

- Assets like server-rendered HTML, CSS, JS, and fonts.
- Public media like images and videos. These typically reside in the public
  folder.

## Server rendered assets

When Next.js is deployed in production, then during the `next build` step all
server rendered assets are put in `.next/static/` folder. Next.js provides an
option to set a CDN to serve these assets. This can be done by setting the
`assetPrefix` property in `next.config.js`, where `ASSET_HOST` is an environment
variable that contains the CDN url.

```js
/* next.config.js */
...

const assetHost = process.env.ASSET_HOST;

const nextConfig = {
  ...
  assetPrefix: isPresent(assetHost) ? assetHost : undefined,
  ...
};

module.exports = nextConfig;
```

[The official doc](https://nextjs.org/docs/app/api-reference/next-config-js/assetPrefix)
has more details about it.

### Result

As shown in the pic below, we had a cache hit. It means the assets are being
served from the configured CDN.

![Cache HITs on server rendered assets](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/serving-nextjs-assets-images-cdn/cache-hits-assets.png)

## Public media

Next.js has
[`<Image>`](https://nextjs.org/docs/pages/api-reference/components/image)
component that comes with out-of-the-box necessary optimizations. If we could
use `<Image>` tag then our CDN problem would also be solved. However the
`<Image>` component
[does not work well with Tailwind CSS](https://github.com/vercel/next.js/discussions/18739),
and we rely heavily on Tailwind CSS.

Therefore, we loaded images from the public folder using the traditional `<img>`
tag.

```jsx
import React from "react";
import dottedBackground from "public/index-dotted-bg";

const HeaderImage = () => (
  <img
    alt="dotted background"
    className="m-0 w-20 md:w-40 lg:w-auto"
    src={dottedBackground}
  />
);
```

However now images are not optimized by Next.js and are served from the server
directly. This is because Next.js
[does not consider](https://nextjs.org/docs/app/api-reference/next-config-js/assetPrefix#:~:text=Files%20in%20the%20public%20folder%3B%20if%20you%20want%20to%20serve%20those%20assets%20over%20a%20CDN%2C%20you%27ll%20have%20to%20introduce%20the%20prefix%20yourself)
media in public folder as static assets to be sent to CDN.

### Configuring CDN

Since we had already set up and configured a CDN using Cloudflare, we set up a
util to prefix the source with our asset host if it is present in the app
environment.

```js
import { isNotNil } from "ramda";

export const prefixed = src =>
  src.startsWith("/") && isNotNil(process.env.ASSET_HOST)
    ? `${process.env.ASSET_HOST}${src}`
    : src;
```

Now we can use `prefixed` in the `<img>` tag.

```jsx
import React from "react";
import { prefixed } from "utils/media";

const HeaderImage = () => (
  <img
    alt="dotted background"
    className="m-0 w-20 md:w-40 lg:w-auto"
    src={prefixed("/index-dotted-bg.svg")}
  />
);
```

We expected that with this configuration, if `ASSET_HOST` value was set up then
the images would be served from the CDN. Otherwise, they would be served from
the server.

The images did load correctly, but the CDN was not able to cache them. All of
the requests resulted in **cache misses**.

![Cache MISSes on server rendered images](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/serving-nextjs-assets-images-cdn/cache-misses-images.png)

### Solution

The CDN was not able to cache the images because the `Cache-Control` header was
not set on the images. This header is set by Next.js when the assets are served
from the `.next/static/` folder, but not when they are served from the `public`
folder. In such cases, we have to explicitly configure the header to be sent
when a request is made from the CDN.

So, we ended up adding a custom `headers` block to the `next.config.js`.

```js
/* next.config.js */
...
const nextConfig = {
  ...
  headers: async () => [
    {
      source: "/:all*(.png|.jpg|.jpeg|.gif|.svg)",
      headers: [
        {
          key: "Cache-Control",
          value: "public, max-age=31536000, must-revalidate",
        },
      ],
    },
  ],
  ...
};

module.exports = nextConfig;
```

This worked. All subsequent requests from the CDN for public images turned into
**cache hits**.

### Result

![Cache HITs on server rendered images](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/serving-nextjs-assets-images-cdn/cache-hits-images.png)

## Conclusion

- Use `assetPrefix` option in `next.config.js` to serve server-rendered assets
  from a CDN.
- Explicitly set `Cache-Control` header in `next.config.js` and ensure image src
  URLs contain the CDN asset host to serve public images from a CDN.

## Links

- [Human page](https://www.bigbinary.com/blog/serving-nextjs-assets-images-cdn)
