Build the Docker image of a Node.js project using Prisma

Photo by Pawel Czerwinski / Unsplash
Photo by Pawel Czerwinski / Unsplash

Prisma is a world-class ORM in the Node.js ecosystem, providing a seamless way to design your database schema, connect your Node.js application to your database, and perform SQL queries.

With a great developer experience from the build locally until the deployment in production. Docker is one of the favorite ways to distribute and deploy applications; when packaging a Node.js application using Prisma, it can be challenging to make it work.

Connect a Node.js application with MongoDB using Prisma
In this post, we will see how to use Prisma to define the Schema of our Mongo database and then perform CRUD actions in a Node.js application with Typescript.

This tutorial shows you how to build the Docker image of a Node.js application that uses Prisma.

Prerequisites

To follow this tutorial, make sure you have the following tools installed on your computer.

Set up the project

As an example of an existing Node.js project, let's use the one we built during the tutorial about creating a Node.js REST API using Prisma and PlanetScale.

The final source code can be found on the GitHub repository. Let's clone the project locally using the Git sparse checkout.


git clone --no-checkout https://github.com/tericcabrel/blog-tutorials.git

cd blog-tutorials

git sparse-checkout init --cone

git sparse-checkout set node-prisma-rest-api

git checkout @

The project is now available locally; let's make it work by running the following commands:


cd node-prisma-rest-api

docker run -d --rm -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=blogdb --name prisma_db -p 3307:3306 mysql:8.0.34

cp .env.example .env
# Set the DATABASE_URL to "mysql://root:secret@localhost:3307/blogdb"

yarn install

yarn prisma migrate dev

yarn ts-node prisma/seed.ts

yarn start

The above commands do the following tasks:

  • Start a Docker container from the Docker image of MySQL.
  • Set the database URL in the environment variables file.
  • Install the Node.js dependencies.
  • Execute the database migrations using Prisma.
  • Seed the MySQL database with some users
  • Start the application.
💡
Unless you work with a database provider such as PlanetScale, delete the variable SHADOW_DATABASE_URL to avoid errors when running database migrations.

The application starts at http://localhost:4500; let's try the application by sending a request to create a post and another one to retrieve all posts:


curl --location 'http://localhost:4500/posts' \
--header 'Content-Type: application/json' \
--data '{
    "authorId": 1,
    "title": "Build a rest api with prism ",
    "content": "This post will show you how to build an awesome api",
    "isFeatured": false,
    "status": "draft"
}'


curl --location 'http://localhost:4500/posts'

You get the following output:

Create and retrieve posts from the API running locally.
Create and retrieve posts from the API running locally.

Build the Docker image

The API works well; let's package it as a Docker image to distribute it efficiently. In the project root directory, create a Dockerfile and add the content below:


FROM node:20-bullseye as builder

RUN mkdir app

WORKDIR /app

COPY . .

RUN yarn install --frozen-lockfile

RUN yarn prisma generate

RUN yarn tsc

FROM node:20-alpine AS runner

ENV NODE_ENV=production

WORKDIR /app

COPY --chown=node:node --from=builder /app/package.json .

COPY --chown=node:node --from=builder /app/build .

RUN yarn install --production && yarn cache clean --all

COPY --chown=node:node --from=builder  /app/node_modules/.prisma/client ./node_modules/.prisma/client

EXPOSE 4500

CMD ["node", "src/index.js"]

The Docker build has two stages:

  • Builder: It runs a Debian image with Node.js 20 installed. Inside this image, the following tasks are performed:
    • Install the Node.js dependencies for the project.
    • Generate the TypeScript types for entities from the Prisma schema.
    • Transpile the project to JavaScript so Node.js can run it natively.
  • Runner: It runs an Alpine image with Node.js 20, which is light compared to the Debian image. Inside this image, the following tasks are performed:
    • Set the environment to production
    • From the Builder stage, copy the package.json and the build folder generated after transpiling the TypeScript code to JavasScript.
    • Install dependencies only for production and clean the Yarn cache.
    • Expose the port 4500 and define the command to start the application.

Run the following command to build the Docker image:


docker build -t node-prisma:latest .

The Prisma Engines

The Docker image of the Node.js application is ready; let's start a container from the image by running the command below:


docker run -it node-prisma:latest

We get the following output:

The Prisma engine error when running the Docker container.
The Prisma engine error when running the Docker container.

This error is related to the Prisma Engine, an essential element of the Prisma ORM.

It is responsible for managing physical connections to the database, receiving instruction from the Prisma client, transforming it to an SQL query, sending it to the database, retrieving the response, and transforming it back to the Prisma client.

The query engine is a different operating system, and it is downloaded when generating the TypeScript types from the Prisma schema. The cause of the error we get is related to this particularity.

In the "builder" stage, we generate the types from the Prisma schema, which runs on Debian bullseye, but the "runner" stage, which is the final stage, runs the application with Alpine Linux.

Check out the list of Prisma Query engines per operating system.

Add the Prisma engines to the Docker image

We must generate the engines for Alpine Linux to fix the Prisma query engine issue. To optimize the build, we will add an intermediary build stage between "builder" and "runner" called "engine-builder".

The build stage "engine-builder" runs on Alpine with Node.js 20 and generates the TypeScript types from the Prisma schema.


FROM node:20-alpine as engine-builder

WORKDIR /app

COPY --chown=node:node --from=builder /app/prisma/schema.prisma ./app/prisma/

RUN npx prisma generate --schema=./app/prisma/schema.prisma

In the build stage "runner," we copy the Prisma client from the "engine-builder" stage instead of the "builder" stage.


COPY --chown=node:node --from=engine-builder  /app/node_modules/.prisma/client ./node_modules/.prisma/client

Here is the complete Dockerfile:


FROM node:20-bullseye as builder

RUN mkdir app

WORKDIR /app

COPY . .

RUN yarn install --frozen-lockfile

RUN yarn prisma generate

RUN yarn tsc


FROM node:20-alpine as engine-builder

WORKDIR /app

COPY --chown=node:node --from=builder /app/prisma/schema.prisma ./app/prisma/

RUN npx prisma generate --schema=./app/prisma/schema.prisma


FROM node:20-alpine AS runner

ENV NODE_ENV=production

WORKDIR /app

COPY --chown=node:node --from=builder /app/package.json .

COPY --chown=node:node --from=builder /app/build .

RUN yarn install --production && yarn cache clean --all

COPY --chown=node:node --from=engine-builder  /app/node_modules/.prisma/client ./node_modules/.prisma/client

EXPOSE 4500

CMD ["node", "src/index.js"]



Build the Docker image from the Dockerfile and run it:


docker build -t node-prisma:latest .

docker run -it node-prisma:latest

Now we get the following error:

The database URL is missing when running the Docker container.
The database URL is missing when running the Docker container.

There is still an error, but not related to the Prisma Query engine but the missing environment variable DATABASE_URL, which is the connection to the database. Run the command with the connection's URL to your remote database.


docker run -it -p 4501:4500 \
-e "DATABASE_URL=mysql://<DB_USER>:<DB_PASSWORD>@<DB_HOST>/<DB_NAME>" \ prisma:latest

Replace <DB_USER>, <DB_PASSWORD>, <DB_HOST> and <DB_NAME> with your database credentials.

You will get an output similar to this:

The Docker container starts successfully.
The Docker container starts successfully.
💡
Using the URL of the database running on your local computer will not work because the Docker container cannot access your computer directly.

Publish and deploy the Docker image

The Docker image is ready to be distributed and pulled from anywhere worldwide. You can publish the image on a Docker registry such as Docker Hub, Amazon Elastic Container Registry, Github Registry, etc...

To publish the image on the Docker Hub, run the following commands


docker login --username=<docker_hub_username>

docker tag node-prisma:latest <docker_hub_username>/node-prisma:latest

docker push <docker_hub_username>/node-prisma:latest

Replace the <docker_username> with your Docker hub username.

Tag and push the Docker image to the Docker Hub.
Tag and push the Docker image to the Docker Hub.
💡
The password asked to authenticate to the Docker Hub is not the one you filled in when creating your account, but instead, a Personal Acess Token generated in your Docker Hub web console. Follow my tutorial to generate your Docker token.

From a Virtual private server, run the following commands to pull to the Docker image and run it:


docker pull tericcabrel/node-prisma:latest

docker run -d --rm -p 4510:4500 --name node-rest-api \
-e "DATABASE_URL=mysql://<DB_USER>:<DB_PASSWORD>@<DB_HOST>/<DB_NAME>" \ tericcabrel/node-prisma:latest

curl --location 'http://localhost:4510/posts' \
--header 'Content-Type: application/json' \
--data '{
    "authorId": 1,
    "title": "Build a rest api with prisma",
    "content": "This post will show you how to build an awesome api",
    "isFeatured": false,
    "status": "draft"
}'


curl --location 'http://localhost:4510/posts'

The last two commands send a request to the API to respectively create a post and retrieve the posts list.

Run the Docker image of the Node.js REST API on a VPS.
Run the Docker image of the Node.js REST API on a VPS.

You successfully deployed the Docker image of a Node.js application using Prisma. The API is unaccessible to the internet; you must expose it from a domain's name and configure a reverse proxy to route the traffic to the container. I show you how to do that in the tutorial below:

Deploy a Node.js application with PM2 and Nginx
This tutorial shows how to deploy a Node.js application on a VPS using PM2 and do a reverse proxy with Nginx on a subdomain to make the application accessible worldwide.

Wrap up

We saw how to build and push the Docker image to the Docker Hub. We pulled it from the Docker Hub from a Virtual Private Server and ran a container of the image.

When building the Docker image of a Node.js application that uses the ORM Prisma, include the suitable Prisma engines for the operating system the applications will run on.

To learn more about the Prisma engine, check out the Prisma official documentation.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.