Building a sane Docker image for Typescript, Yarn Workspaces and Prisma 2
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.jsonandtsconfig.jsonfrom the root, thebackendpackage and thecommonpackage. 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
backenddependencies somewhere else. This will allow us to copy them later in the runner stage. I tried usingnpm prune --productionbut the result was not as good as a clean install. You might need to pass--ignore-scriptsto yarn if you have stuff in apostinstallthat doesn’t work in production (it is usual with prisma to generate the client in thepostinstall). Also worth noting, we use the-Lflag incpto make sure the dependencies of our common packages are also copied since it will be linked in thebackenddependencies with a symlink. - Fourth, we install the dev dependencies and we then go on to build each package in order:
common,prisma clientandbackend. 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 generatedprisma clientand the compiledcommon. We finally copy the compiled backend code and thepackage.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-pathsfor 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.
