Bring Your Own Next.js
Tags: nextjs, docker, containers
Published: 2024-06-16 12:47:40

---
date: 2024-05-22 20:14:00 UTC
canonical_url: https://richardkovacs.dev/blog/bring-your-own-nextjs
---

I am a huge fan of Next.js, but I follow a strict personal policy of only using it for the front end — no server actions, no edge functions, nothing else outside TSX. Of course, I had a very good reason to do so: vendor lock-in.

Vendor lock-in is when you are using a specific product that only works on the provider's platform. In my case, the product is Next.js, and the platform is Vercel. When you are using a product like this, your fate is totally in the hands of the provider. They can increase your costs, deprecate features, have regular outages, etc. And if you are dependent on them, taking your stuff somewhere else is extremely painful, if not impossible.

While the frontend part is easy to move elsewhere, I cannot say the same about lambda functions. They work the best on Vercel as Vercel has the architecture ready for it. I have only found one viable alternative so far in the form of AWS Amplify, which takes a quarter of a lifetime to set up correctly compared to deploying to Vercel with a single click.

But even if AWS caught up to Vercel in simplicity, they would be one step behind at each new Next.js release.

## What Happened?

My view on the above totally changed recently when I stumbled upon a specific part in the docs about self-hosting Next.js. It isn't brand new; they probably have had it there for ages, but it only caught my attention last week.

Apparently, you have the option to host Next.js yourself. You just need a Node environment or Docker. Of course, the docs mention that you can achieve the best performance if you host your site on Vercel, but I am totally okay with that. Honestly, the best thing Vercel could have done to Next.js is to enable anyone to take their app wherever they prefer, thus eliminating vendor lock-in. An awesome move! Now, let's see how it works in practice.

## Hosting Next.js Locally with Node.js

I know the development mode is also running locally on Node, but I am speaking about the production version. Hosting the app locally this way is pretty straightforward. You are actually only two commands away from it: `next build` and `next start`.

The first command will build your entire application and put it in the `.next` folder at the root of your project. If you are interested, the app will be located in the `.next/server` folder. You will find everything here: frontend, lambda functions, edge functions, and everything your app contains.

The second command will start and continuously serve the app from here.

And that's it. You now have a full-stack Next.js application running on your machine in production mode. You can take this to any provider you prefer. Just build the app and make sure to start it. Only Node.js is required.

However, this is not the only option the docs provide. You have another alternative.

## Containerized Next.js

If you are a fan of microservices or a regular user of a provider that supports Docker containers, a containerized Next.js application might better suit your needs. The docs contain steps for this setup as well, although it only works out of the box if you clone their example on GitHub. If you want to dockerize an existing application, follow the guide below.

I chose the [Docker Compose example](https://github.com/vercel/next.js/tree/canary/examples/with-docker-compose) from the official Next.js repository. I prefer Compose because settings are much more readable than in a single-line docker command.

This blog was written in Next.js, and I currently host it on Vercel. The blog posts are written in Markdown, and when you open one of the posts like this one, it is server-side rendered into the HTML you are looking at right now, so this blog was a good candidate for containerization as it contains multiple client-side rendered pages but also an SSR page.

### Standalone Output

One of the first steps is to change your [output](https://nextjs.org/docs/pages/api-reference/next-config-js/output) to standalone in your `next.config.js`.

```js
module.exports = {
  output: 'standalone',
}

```

This setting ensures that only the necessary files and dependencies are copied to the output folder, making the result much smaller.

### Dockerfile

The next step is to obtain a `Dockerfile`. I used the [`Dockerfile`](https://github.com/vercel/next.js/blob/canary/examples/with-docker-compose/next-app/prod.Dockerfile) from the [Docker Compose example](https://github.com/vercel/next.js/tree/canary/examples/with-docker-compose). You can find plenty of [other examples](https://github.com/vercel/next.js/tree/canary/examples) in the repository, such as plain Docker option, Docker with multi-stage and multi-environment setups, etc.

The `Dockerfile` was almost perfect, but it needed some tweaking to work with this blog's source. The `builder` step copies the source and some config files into the image, but since I am using Tailwind, I had to add two additional lines below [line 22](https://github.com/vercel/next.js/blob/c9291d6dd57e08c525733b7187a3b4256bc5f8af/examples/with-docker-compose/next-app/prod.Dockerfile#L22):

```Dockerfile
COPY postcss.config.js .
COPY tailwind.config.js .

```

I also had to add another line to this list to copy my markdown pages.

```Dockerfile
COPY _posts ./_posts

```

If your application contains other config files and/or folders, make sure that they are copied as well.

I also use shadcn/ui components, but the `components.json` was not required since that file is only needed when generating a new component from the CLI. The production version does not use it, as the file is not needed during build time.

I also have some environment variables in the repository. I had to add two more lines below [line 29](https://github.com/vercel/next.js/blob/c9291d6dd57e08c525733b7187a3b4256bc5f8af/examples/with-docker-compose/next-app/prod.Dockerfile#L29) for each required at build time. I had to do the same for each run time variable below [line 66](https://github.com/vercel/next.js/blob/c9291d6dd57e08c525733b7187a3b4256bc5f8af/examples/with-docker-compose/next-app/prod.Dockerfile#L66). For example, if your app is using `EXTERNAL_URL`, you have to add the following two lines in the corresponding places:

```Dockerfile
ARG EXTERNAL_URL
ENV EXTERNAL_URL=${EXTERNAL_URL}

```

With these modifications, the `Dockerfile` was ready.

### Docker Compose

I started with the [Docker Compose](https://github.com/vercel/next.js/blob/canary/examples/with-docker-compose/docker-compose.prod.yml) present in the example, but it also needed some minor changes. Since I put both the `Dockerfile` and the `docker-compose.yml` in the root of the repo, I had to change the `context` of the `build` setting to `./`. I also had to change the `dockerfile` parameter to simply `Dockerfile`.

In the next step I had to define the environment variables. Going with the previous example, if you rely on `EXTERNAL_URL`, your `build` step needs the following `args`:

```yaml
EXTERNAL_URL: ${EXTERNAL_URL}

```

Here is how the final compose file looks like:

```yaml
version: "3"

services:
  next-app:
    container_name: next-app
    build:
      context: ./
      dockerfile: Dockerfile
      args:
        EXTERNAL_URL: ${EXTERNAL_URL}
    restart: always
    ports:
      - 3000:3000
    networks:
      - my_network

networks:
  my_network:
    external: true

```

## How to Start a Containerized Next.js?

If you have everything set up correctly, you can start your containerized application by running `docker compose build` followed by `docker compose up`.

⚠️ Remember that this is an optimized production build. Auto restart on file changes won't work, as the goal of this build is to be deployed to a provider.

## Caveats

Here are some pitfalls I faced when porting my app to Docker. Pay extra attention to these as fixing them required the most time in my case.

### Environment Variables

Make sure that you define your environment variables in every location. Build-time variables should be present in the `builder` step, and run-time variables should be added to the `runner` step. Additionally, add them to the compose file. Since the production build is optimized, it won't tell you that this was the problem; it will just throw an error.

### Sharp

You will probably see an error saying the `sharp` package is required in production builds. Next.js uses this package under the hood when you define an `Image` from the `next/image` package. However, simply installing it in some cases won't solve the problem as there is an ongoing [version mismatch problem](https://github.com/vercel/next.js/discussions/40125).

One option is to define the following variable:

```Dockerfile
ENV NEXT_SHARP_PATH=/app/node_modules/sharp

```

If the variable above does not help, ensure your `next` package is updated to the latest version. Updating `next` to version `14.2.3` fixed the error, without needing the environment variable. The `sharp` version that worked was `0.33.3`.

### Missing Folders, Configs

Double-check that every required folder and config file is copied to the images. As I am using Tailwind, I needed both `postcss.config.js` and `tailwind.config.js`. I also had to add my `_posts` folder. Check which folders and configs your app relies on and copy each.

### Standalone Build

Finally, if you are using Docker, ensure the standalone output mode is correctly set in your `next.config.js`.

## Wrapping It Up

Vendor lock-in is a sneaky move, and I'm glad I was wrong about Vercel. Offering the option to take your app to another provider builds trust, which is essential to keeping your users long-term. At least, that's my view on the topic.

With that being said, I am still hosting this blog with them, and I don't plan to change this anytime soon. However, there are other private projects where I need the self-hosted option, which is now fairly easy based on the steps above.

If you still have questions left or want to add something to this post, let me know on any of the social channels I am active on. You can find them down below.