Building a sane Docker image for Typescript, Yarn Workspaces and Prisma 2

Emile Fugulin
3 min readApr 18, 2020

--

EDIT 1: As of Beta 4, Prisma now creates a new folder called .prisma which contains the generated client. This is reflected in the current Dockerfile.

The other day I was working on the deployment of the new service I am building for a client. As is usual these days, I started by writing a Dockerfile to containerize this new service so it could be deployed (on GCP Cloud Run for this project). It usually doesn’t take me long to build a good image with proper caching and minimal size (I am a bit of a Docker nerd with a passion for optimized images 😀).

But this setup is particularly tricky because of the three following facts:

  • Prisma generates a client inside the node_modules (and requires some dev dependencies to do so)
  • We have a common package that is not published to central repository (managed with yarn workspaces)
  • We don’t want the image to contain Typescript code (only the generated Javascript)

I could not find any good resource online on how to create an efficient image for this specific setup, so I decided to write this blog post to help others who might face a similar challenge. Without further ado, here is the full Dockerfile I came up with:

Let’s go over this beast! 😅

  • First, you can see that it’s a multistage build. We define a base image from node buster slim (Prisma does not currently build binaries for alpine images). I prefer buster since it’s the latest debian version. We also need to install openssl, because the slim version doesn’t ship with it anymore (this is the only step I don’t really like and might rework to reduce size).
  • Second, we create a builder stage that will first copy the package.json and tsconfig.json from the root, the backend package and the common package. This is necessary for proper caching of layers (always pull dependencies before copying the code)
  • Third, we install the production dependencies for all our packages and we copy the backend dependencies somewhere else. This will allow us to copy them later in the runner stage. I tried using npm prune --production but the result was not as good as a clean install. You might need to pass --ignore-scripts to yarn if you have stuff in a postinstall that doesn’t work in production (it is usual with prisma to generate the client in the postinstall). Also worth noting, we use the -L flag in cp to make sure the dependencies of our common packages are also copied since it will be linked in the backend dependencies with a symlink.
  • Fourth, we install the dev dependencies and we then go on to build each package in order: common, prisma client and backend. This is relatively straight forward , you just need to be careful of the order otherwise the Typescript compiler will complain.
  • Fifth, we create a runner stage and we copy all the dependencies we need. That means the production node_modules, the generated prisma client and the compiled common. We finally copy the compiled backend code and the package.json (only necessary if you need scripts).
  • Sixth, we drop the privileges of the runner and we set the command to start the server (we use tsconfig-paths for our absolute paths).

So that’s it! With this, you have an optimized image since you will only push the runner stage in production and it should still be fast to build if the dependencies don’t change and you cache the builder stage.

--

--