DEV Community

Marc Stammerjohann for notiz.dev

Posted on • Originally published at notiz.dev on

Dockerizing a NestJS app with Prisma and PostgreSQL

Docker 🐳 enables you to build consistent containers of your applications for your development, testing and production environments. In this post you will dockerize a NestJS 😸 application with Prisma connecting to a PostgreSQL 🐘 database.

Requirements for this post are

  1. Docker installed
  2. NestJS application with Prisma

You can find the full source code on GitHub.

Use this prisma schema to follow along:

datasource db {
  provider = "postgresql"
  url = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Food {
  id Int @id @default(autoincrement())
  name String
}

And a .env file in your prisma directory for a dummy PostgreSQL connection url:

DATABASE_URL=postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public

TL;DR Multi-stage Dockerfile

Create a Dockerfile in the root of your Nest application

touch Dockerfile

Open the Dockerfile and use the multi-stage build steps πŸ€™

FROM node:12 AS builder

# Create app directory
WORKDIR /app

# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
COPY prisma ./prisma/

# Install app dependencies
RUN npm install
# Generate prisma client, leave out if generating in `postinstall` script
RUN npx prisma generate

COPY . .

RUN npm run build

FROM node:12

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/dist ./dist

EXPOSE 3000
CMD ["npm", "run", "start:prod"]

But wait… what is going in the Dockerfile πŸ€”β“ See the breakdown for each step below.

Don't forget to create a .dockerignore file next to your Dockerfile:

node_modules
npm-debug.log

The COPY command ignores those local files and folder and won't copy them into your Docker image to prevent overwriting your installed modules in your image.

Your application structure should look like this:

Project structure

Breakdown of the multi-stage Dockerfile

Let's breakdown the Dockerfile step-by-step

πŸ— Builder Image

FROM node:12 AS builder

The first line tells Docker to use the latest LTS version 12 for node as the base image to build the container from. To optimize the container image size you are using the multistage-build and assign a name to your base image AS builder.

Note : Before updating to a newer version of node check the support of Nest, Prisma and other dependencies

🧰 Working directory

# Create app directory
WORKDIR /app

Create the working directory for your application which stores your code. All commands (RUN, COPY) are executed inside this directory.

πŸ“¦ Installation

# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
COPY prisma ./prisma/

# Install app dependencies
RUN npm install
# Generate prisma client, leave out if generating in `postinstall` script
RUN npx prisma generate

Next you need to install your app dependencies inside the Docker image. package.json and package-lock.json are copied over. Generating the Prisma Client requires the schema.prisma file. COPY prisma ./prisma/ copies the whole prisma directory in case you also need the migrations.

Note : Only package*.json and prisma directory is copied in this step to take advantage of the cached Docker layers.

Install all dependencies RUN npm install (dev too). This allows you to build the Nest application inside the Docker image. Now its also the time to generate the Prisma Client. Leave this step out if you are generating the client in the a postinstall script.

βš™οΈ Build app

COPY . .

RUN npm run build

To build your Nest application copy all of your source files (exceptions in .dockerignore) into the Docker image. Now it's time to build your app RUN npm run build.

πŸ‘Ÿ Run your app

FROM node:12

The second FROM is the second stage in the multi-stage build and is used to run your application.

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/dist ./dist

Copy from your builder image only files and folders required to run the Nest app.

EXPOSE 3000
CMD ["npm", "run", "start:prod"]

Nest apps usually bind to port 3000, EXPOSE the same port for your Docker image. Last step is the command to run the Nest application using CMD.

Build and run your image

Enter the following command in the directory of your Dockerfile. Give your build image a name using the -t flag to easily start, stop and remove it.

# give your docker image a name
docker build -t <your username>/nest-api .

# for example
docker build -t nest-api .

After your Docker image is successfully build start it with this command

docker run -p 3000:3000 --env-file prisma/.env -d <your username>/nest-api 

Prisma Client requires the DATABASE_URL environment variable which you pass using the --env-file prisma/.env flag. Use this .env file for additional environment variables (Port, JWT Secret etc.) or copy it into your root folder.

Open up localhost:3000 to verify that your Nest app is running with Docker.

Add docker-compose with PostgreSQL

Docker Compose allows you to define and run multiple Docker container together.Here you are setting up a Docker compose file for the Nest application and a PostgreSQL database.

Create the Docker compose file

touch docker-compose.yml

Add following services to docker.compose.yml

version: '3.7'
services:
  nest-api:
    container_name: nest-api
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 3000:3000
    depends_on:
      - postgres
    env_file:
      - .env

  postgres:
    image: postgres:12
    container_name: postgres
    restart: always
    ports:
      - 5432:5432
    env_file:
      - .env
    volumes:
      - postgres:/var/lib/postgresql/data

volumes:
  postgres:
    name: nest-db

The first service nest-api is building the Docker image based on your Dockerfile for your Nest app with Prisma. The second service is creating a postgres database using the postgres Docker image in version 12. For the Postgres image set POSTGRESQL_USER, POSTGRESQL_PASSWORD and POSTGRES_DB environment variables in a .env file next to your docker-compose.yml

POSTGRESQL_USER=prisma
POSTGRESQL_PASSWORD=topsecret
POSTGRES_DB=food

To connect to the PostgreSQL database Docker image configure the DATABASE_URL in your .env file. Fill in your values into the Postgres connection url format

postgresql://USER:PASSWORD@HOST:PORT/DB?schema=NAME&sslmode=prefer

In this example add the following variable to the .env file. The HOST is when connecting from another Docker image either the service name or the container name - both postgres.

DATABASE_URL=postgresql://prisma:topsecret@postgres:5432/food?schema=food&sslmode=prefer

Time πŸ•™ to start your Nest app and Postgres Docker image. Make sure the ports 3000 and 5432 are not in use already.

docker-compose up
# or detached
docker-compose up -d

You should have the following two docker containers started

Docker container started by docker-compose

Open again localhost:3000 to verify that your Nest app is running with Docker. Also verify that your endpoints using the Prisma Client have access to the Postgres DB.

Prisma Migrate Postgres Docker Container

Replace the host postgres with localhost if you want to perform Prisma migrations locally of your Postgres Docker container. Update the DATABASE_URL in prisma/.env to

DATABASE_URL=postgresql://prisma:topsecret@localhost:5432/food?schema=food&sslmode=prefer

Now you can run npx prisma migrate save --experimental and npx prisma migrate save --experimental or even seed the database if you like.

Perfect, now sit back and relax 🏝 and let Docker do the work for you.

Top comments (2)

Collapse
 
cbwilliamsnh profile image
Chuck Williams

I've (for the most part) followed this post to dockerize my NestJS/prisma app. The image builds without issue, however when I run the image, I get the following error/stack trace:

starting app...
Starting the server...
[Nest] 17  - 08/15/2023, 2:54:36 PM     LOG [NestFactory] Starting Nest application...
[Nest] 17  - 08/15/2023, 2:54:36 PM   ERROR [ExceptionHandler] @prisma/client did not initialize yet. Please run "prisma generate" and try to import it again.
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues
Error: @prisma/client did not initialize yet. Please run "prisma generate" and try to import it again.
In case this error is unexpected for you, please report it in https://github.com/prisma/prisma/issues
    at new PrismaClient (/app/node_modules/.prisma/client/index.js:43:11)
    at new PrismaService (/app/dist/src/prisma/prisma.service.js:12:21)
    at Injector.instantiateClass (/app/node_modules/@nestjs/core/injector/injector.js:351:19)
    at callback (/app/node_modules/@nestjs/core/injector/injector.js:56:45)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Injector.resolveConstructorParams (/app/node_modules/@nestjs/core/injector/injector.js:136:24)
    at async Injector.loadInstance (/app/node_modules/@nestjs/core/injector/injector.js:61:13)
    at async Injector.loadProvider (/app/node_modules/@nestjs/core/injector/injector.js:88:9)
    at async /app/node_modules/@nestjs/core/injector/instance-loader.js:56:13
    at async Promise.all (index 3)
Enter fullscreen mode Exit fullscreen mode

When I run the image with a shell and look for the prisma client, I see it right where it should be:

/app # ls -R node_modules/@prisma/
node_modules/@prisma/:
client           engines          engines-version

node_modules/@prisma/client:
LICENSE           edge.d.ts         extension.d.ts    generator-build   index.d.ts        package.json      scripts
README.md         edge.js           extension.js      index-browser.js  index.js          runtime

node_modules/@prisma/client/generator-build:
index.js

node_modules/@prisma/client/runtime:
binary.d.ts         data-proxy.d.ts     edge-esm.js         index-browser.d.ts  library.d.ts
binary.js           data-proxy.js       edge.js             index-browser.js    library.js

node_modules/@prisma/client/scripts:
colors.js                 default-edge.js           default-index.d.ts        get-packed-client.js      postinstall.js
default-deno-edge.ts      default-index-browser.js  default-index.js          postinstall.d.ts

node_modules/@prisma/engines:
LICENSE                                                 libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node  scripts
README.md                                               package.json
dist                                                    schema-engine-linux-musl-arm64-openssl-3.0.x

node_modules/@prisma/engines/dist:
index.d.ts  index.js    scripts

node_modules/@prisma/engines/dist/scripts:
localinstall.d.ts  localinstall.js    postinstall.d.ts   postinstall.js

node_modules/@prisma/engines/scripts:
postinstall.js

node_modules/@prisma/engines-version:
LICENSE       README.md     index.d.ts    index.js      package.json
Enter fullscreen mode Exit fullscreen mode

I'm running on a M1 MacBook (latest ventura 13.4.1(c). Here is my Dockerfile:

###################
# BUILD FOR LOCAL DEVELOPMENT
###################

FROM node:20-alpine As development

# Create app directory
WORKDIR /app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY --chown=node:node package*.json ./
COPY --chown=node:node prisma ./prisma

# Install app dependencies using the `npm ci` command instead of `npm install`
RUN npm ci
RUN npm install @prisma/client

# Bundle app source
COPY --chown=node:node .env ./.env
COPY --chown=node:node . .

# Use the node user from the image (instead of the root user)
USER node

###################
# BUILD FOR PRODUCTION
###################

FROM node:20-alpine As build

WORKDIR /app

COPY --chown=node:node package*.json ./

# In order to run `npm run build` we need access to the Nest CLI which is a dev dependency. 
# In the previous development stage we ran `npm ci` which installed all dependencies, so we 
# can copy over the node_modules directory from the development image
COPY --chown=node:node --from=development /app/node_modules ./node_modules

COPY --chown=node:node .env ./.env
COPY --chown=node:node . .

# Run the build command which creates the production bundle
RUN npx prisma generate --schema ./prisma/schema.prisma
RUN npm run build

# Set NODE_ENV environment variable
ENV NODE_ENV production

# Running `npm ci` removes the existing node_modules directory and 
# passing in --only=production ensures that only the production 
# dependencies are installed. This ensures that the node_modules 
# directory is as optimized as possible
RUN npm ci --only=production && npm cache clean --force

USER node

###################
# PRODUCTION
###################

FROM node:20-alpine As production

# Copy the bundled code from the build stage to the production image
COPY --chown=node:node --from=build /app/node_modules /app/node_modules
COPY --chown=node:node --from=build /app/dist /app/dist
COPY --chown=node:node --from=build /app/.env /app/.env

# Expose the server port
EXPOSE 9090

# Start the server using the production build
CMD [ "node", "/app/dist/src/main.js", "serve" ]
Enter fullscreen mode Exit fullscreen mode

Am I missing something or did I find a bug?

Thank you in advance,
Chuck

Collapse
 
cbwilliamsnh profile image
Chuck Williams

Apparently it is this line that's munging up the works:

RUN npm ci --only=production && npm cache clean --force

With that line commented out, everything runs just fine. However, as you would expect, the image size is about 2x larger.

Chuck