Dockerizing Next.js with Prisma for Production Environments
Tags: nextjs, docker, javascript, tutorial
Published: 2022-03-08 16:19:44

[Here is the sample repository for this blog post](https://github.com/CaptainChemist/docker-prisma-prod)

[Next.js](https://nextjs.org/) is a phenomenal framework for building SEO friendly, performant webpages with React. For static pages, Next.js is enough to create your web page, but when you need to store persistent state such as when you have users, or perhaps blog pages that are dynamically being created once the web page has been deployed, you need a database to keep track of the various changes in state that the web page will undergo. [Prisma](https://www.prisma.io/?utm_source=Prisma%20Ambassador&utm_medium=Blog%20post&utm_campaign=Prisma%20AP%20Stephen%20Jensen) is a library that will create a connector with your database and allow you to easily perform CRUD (create, read, update and delete) operations whenever your backend needs to.

The combination of Next.js and Prisma is a powerful one, and I’ve created [blog posts](https://www.codemochi.com) and [courses](https://courses.codemochi.com/end-to-end-react-with-prisma-2/) if you are interested in how to create a complete web application from scratch, but for this post we will discuss how to deploy Prisma and Next.js in a production docker container.

If you haven’t used Docker before, it is a containerization technology that allows you to reproducibly build and run your code in a way that will consistently run across all platforms, both on your computer and up in the cloud. The primary configuration that we need to do with Docker is to create a `Dockerfile` that essentially can be thought of as the command line steps that you’d type into your terminal in order to build your Next.js and Prisma app.

We will build up our production image in [multiple stages](https://docs.docker.com/develop/develop-images/multistage-build/) which will allow us to take the approach of building the code in one image that is all loaded up with the development npm dependencies and then copy the built code into a clean production image to dramatically save on space.

The four main commands used in a `Dockerfile` are the following:

`FROM`: this is your starting spot for building your docker image. The first time that you use this in a `Dockerfile`, you will be pulling from an already existing image on the internet. When you have multiple stages, it is good practice to label the stage using the `AS` followed by the name. Then, later in the `Dockerfile` you can use `FROM` to import the current state of that layer, which we’ll talk about in a bit.

`RUN`: used for running any commands just like you would from the command line. Keep in mind that the shell you are in is dictated by the base image that you are loading. For example, alpine images are widely used due to their small size but they also use the `sh` shell rather than `bash`, so if you are using alpine make sure that your `RUN` commands are `sh` compatible. In this example below, we will use the `slim` family of docker images as our base which uses `bash` as its shell. This makes installing Prisma dependencies much easier.

`WORKDIR`: This will set the current working directory to whatever path is specified.

`COPY`: Takes two or more parameters, the first up through the second to last parameters are paths to the desired file(s) or folder(s) on the host. The last parameter is the destination path for where those files should be copied into.

There are two other commands you sometimes see in Dockerfiles, but since they can also be configured with docker-compose, kubernetes or whatever your hosting provider is, they are less important:

`EXPOSE`: allows you to explicitly open certain ports in the container. Can be overridden when running the container.

`CMD`: indicates the command that Docker runs when the container starts up. Can also overridden when run.

Armed with those basics, let’s take a look at the start of our `Dockerfile`. The goal with creating this base docker image is to have everything that both our development and production images without anything more. There will be 4 layers that we create to our `Dockerfile`:

1. `base` layer has system dependencies, package.json, yarn.lock, and .env.local file.
2. `build` layer starts with `base` and installs all dependencies to build `.next` directory that has all of the site’s code ready for use.
3. `prod-build` layer starts with `base` and installs production dependencies only.
4. `prod` layer starts with `base` and copies production dependencies from `prod-build`, `.next` folder from `build`

5. Create the `base` layer

```Dockerfile
FROM node:lts-buster-slim AS base
RUN apt-get update && apt-get install libssl-dev ca-certificates -y
WORKDIR /app

COPY package.json yarn.lock ./
```

This starts with a slim version of the long term stable version of node and labels it `base`. Going with the slim variety allows the base image to only be 174MB while the full-blown image is 332MB. Alpine images are even smaller- around 40MB but since the shell is `sh` rather than `bash`, I ran into problems getting everything needed for Next.js and Prisma to compile properly. (Found a way to get alpine to work? Let me know in the comments!)

In any case, we start with Buster Debian base image that has node lts preinstalled, and then we run `apt-get update` to ensure that all of our package lists are up to date. We then install `libssl-dev` and `ca-certificates` which are dependencies of Prisma and then set the working directory as `/app`.

2. Create the `build` layer

By then creating a new `FROM` designation, we are saving off those first set of steps under the layer `base`, so that any steps created from here on out get saved to the `build` layer, rather than the `base` layer.

From the top:

```Dockerfile
FROM node:lts-buster-slim AS base
RUN apt-get update && apt-get install libssl-dev ca-certificates -y
WORKDIR /app

COPY package.json yarn.lock ./

FROM base as build
RUN export NODE_ENV=production
RUN yarn

COPY . .
RUN yarn run prisma:generate
RUN yarn build
```

Running `yarn` does an install of all the packages that we have in our `package.json` which we copied in during the `base` step. From there, we can copy in our entire next.js app to the `/app` folder with the command `COPY . .`. Once we have our dependencies, we can run the `prisma:generate` command which we define in the `package.json` as `prisma generate`. This generates the client library in our `node_modules` folder that’s specific to the Prisma schema that we’ve already defined in `prisma/schema.prisma`.

3. Create the `prod-build` layer

Now that we have our site’s code built, we should turn to installing the production dependencies so we can eliminate all the packages that are only for development. Picking up with the `base` image, we install the production npm packages, and then copy in the Prisma folder so that we can generate the Prisma library within the `node_modules` folder. To ensure that we keep this production node modules folder intact, we copy it off to `prod_node_modules`.

```Dockerfile
FROM base as prod-build

RUN yarn install --production
COPY prisma prisma
RUN yarn run prisma:generate
RUN cp -R node_modules prod_node_modules
```

4. Create the production layer

Now that we’ve created all of our build layers, we are ready to assemble the production layer. We start by coping `prod_node_modules` over to the app's `node_modules`, next we copy the `.next` and `public` folders which are needed for any Next.js apps. Finally, we copy over the `prisma` folder, which is needed for Prisma to run properly. Our `npm start` command is different from the development `npm run dev` command because it runs on port 80 rather than 3000 and it is also using the site built out of `.next` rather than hot-reloading the source files.

```Dockerfile
FROM base as prod

COPY --from=prod-build /app/prod_node_modules /app/node_modules
COPY --from=build  /app/.next /app/.next
COPY --from=build  /app/public /app/public
COPY --from=build  /app/prisma /app/prisma

EXPOSE 80
CMD ["yarn", "start"]
```

In all, by creating a layered approach we can save often save a 1GB or more off the image size which can really speed up the deployment to AWS Fargate, or whatever hosting platform that you choose to do.

Here’s the final full `Dockerfile`:

```Dockerfile
FROM node:lts-buster-slim AS base
RUN apt-get update && apt-get install libssl-dev ca-certificates -y
WORKDIR /app

COPY package.json yarn.lock ./

FROM base as build
RUN export NODE_ENV=production
RUN yarn

COPY . .
RUN yarn run prisma:generate
RUN yarn build

FROM base as prod-build

RUN yarn install --production
COPY prisma prisma
RUN yarn run prisma:generate
RUN cp -R node_modules prod_node_modules

FROM base as prod

COPY --from=prod-build /app/prod_node_modules /app/node_modules
COPY --from=build  /app/.next /app/.next
COPY --from=build  /app/public /app/public
COPY --from=build  /app/prisma /app/prisma

EXPOSE 80
CMD ["yarn", "start"]
```

## Running Noted: a cryptocurrency tracker locally and in production

The sample project used for this repo is a simple cryptocurrency tracking application that allows you to add how much of each cryptocurrency you have and it will tell you the current worth based on the market prices. You should create a `.env.local` that looks like this:

```.env.local
DATABASE_URL=file:dev.db
#CMC_PRO_API_KEY=000-000-000-000-000
```

The `CMC_PRO_API_KEY` is optional but if set will pull the latest currency data for the top cryptocurrencies using CoinMarketCap. If you'd like to use it, sign up for a free account over at [CoinMarketCap](https://coinmarketcap.com/api/) and replace the blank api key with your actual api key and remove the `#` from the start of the variable definition. If you choose not to use the api, the app will populate with some default coins and prices.

To run it locally, feel free to delete any `prisma/dev.db` file and `prisma/migrations` folder that you already have. Next run `npm install`. Ideally your version of node will match the lts version used in the docker images. You can use `nvm` to set the version and `node --version` to check that they are the same. Then you can run`npm run prisma:generate` which will generate the library followed by `npm run prisma:migrate` to create a `dev.db` file.

From there, you have two options. First, you can run it locally without docker which will allow you to make changes and see them instantly change in your app. This works best for the development stage of things. To run this, run `npm run dev`.

To run it locally in the docker environment, first you need to build the image with `docker-compose build`. Next, you can run `docker-compose up` to actively run the image. There is a volume set up so that it will utilize the `prisma/dev.db` folder that you have mounted on your host. I'll discuss in a minute why this is not ideal, but in a pinch this can be used to run your webapp in a production environment because the `dev.db` file is being mounted on your host which will mean that it will persist when the containers crash or the machine or docker has been restarted.

The downsides to running the app with a local `dev.db` file is that there are no backups or redundancies. For a true production environment, the datasource should be migrated from `sqlite` to `postgresql` or `mysql` connectors with the `url` being changed to a database connection string. Here's an example of how you'd switch to `postgresql`.

```schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
```

```.env
DATABASE_URL="postgresql://your_user:your_password@localhost:5432/my-prisma-app?schema=public"
```

For the purposes of this tutorial we wanted to keep it with `sqlite` because the local development is just so much easier and it is essentially a drop-in replacement to switch over to a more production friendly environment.

Stay tuned for a future blog post where we go through all of the inner-workings of this app and show how Prisma can be used with Next.js to create a nimble fullstack web application!

Originally posted at [Code Mochi](https://www.codemochi.com/blog/2022-01-01-dockerizing-nextjs-with-prisma-for-production).