Dockerizando NestJS + Prisma 7 + Yarn
Aplicação Utilizada
A aplicação utilizada para a demonstração será esta — o blog que você está lendo neste momento. A versão do Prisma ORM utilizada é a 6.13, porém estamos utilizando o generator prisma-client, que se tornará o padrão na versão 7.0 e exige a opção output explicitamente, diferente do prisma-client-js, que usava o node_modules como local padrão. Para futura referência, o commit do momento em que escrevo é o cf0483e.
Assumindo que você tenha corretamente instalado suas dependências com separação entre dev e prod, o projeto se beneficiará de uma build em duas etapas — uma para o build e outra, final, para a execução. Vejamos o Dockerfile.
Dockerfile
Antes de continuar, é importante notar que no projeto há um .dockerignore que impede que diretórios indesejados ou desnecessários sejam copiados para a imagem:
dist/ # arquivos transpilados localmente
node_modules/ # dependências locais previamente instaladas
...
Finalmente, eis o Dockerfile:
# Build stage
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json yarn.lock ./
COPY prisma ./prisma/
RUN yarn install --frozen-lockfile
COPY . .
RUN npx prisma generate && yarn build
# Production stage
FROM node:24-alpine AS production
WORKDIR /app
COPY package.json yarn.lock ./
COPY prisma ./prisma
RUN yarn install --frozen-lockfile --production && yarn cache clean
COPY views ./views
COPY public ./public
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules/prisma/libquery_engine-linux-musl-openssl-3.0.x.so.node ./dist/src/generated/prisma/client
EXPOSE 3000
CMD ["sh", "-c", "npx prisma migrate deploy && yarn start:prod"]
Vamos entender o que está sendo feito: Multi-stage Build
O Docker nos permite utilizar múltiplos containers, de imagens iguais ou não, para construir uma imagem final. O nome disto é multi-stage build. Esta prática é útil quando o projeto possui dependências que só são necessárias durante o desenvolvimento da aplicação; no caso do Typescript, podemos apontar o próprio transpilador da linguagem como tal. Outras dependências de desenvolvimento incluem, usualmente, linters e frameworks de testes.
Utilizando o multi-stage build, podemos usar uma imagem (linhas 2-13) para transpilar o código, onde instalaremos todas as dependências disponíveis, e outra (linhas 16-33) para executar a aplicação, onde instalaremos apenas as dependências necessárias para execução.
Build stage
Eis o que é feito no estágio de build, passo a passo:
- Utilizamos a imagem node-alpine como base por seu tamanho pequeno;
- É definido o diretório de operação: /app;
- Arquivos necessários para a instalação de dependências e geração do cliente prisma são copiados da sua máquina para o container: package.json e prisma/;
- Dependências são instaladas com o yarn;
- O resto dos arquivos são copiados para o container (todos, exceto os definidos no .dockerignore);
- O cliente prisma é gerado e é realizado o build da aplicação.
Production stage
Agora, estaremos trabalhando com um novo container. O container de build ainda nos é acessível: para copiar arquivos do container de build para o novo, podemos utilizar COPY --FROM=builder <src> <dest>
— note que builder é o nome que demos para nosso container de build.
Vamos observar o que acontece no estágio de produção. Note que os primeiros passos serão similares:
- Também utilizamos a imagem node-alpine como base;
- É definido o diretório de operação: /app;
- Arquivos necessários para a instalação de dependências e geração do cliente prisma são copiados da sua máquina para o container: package.json e prisma/;
- Agora, ao instalarmos as dependências com o yarn, utilizaremos a flag --prod para excluir dependências de desenvolvimento. Após isso, limparemos o cache do yarn com
yarn cache clean
; - Para a execução do nosso app, os arquivos contidos nos diretórios views/ e public/ são necessários, então os copiamos de nossa máquina para o container final.
- Trazemos os arquivos que foram transpilados do container de build para o container de prod.
- O último arquivo que trazemos do builder é necessário para que o prisma seja executado em ambientes como o Alpine, que usam musl em vez da mais amplamente utilizada glibc.
- Por fim, expomos a porta em que nossa aplicação está rodando e definimos o comando de entrada na imagem que será gerada.
Construindo a Imagem
Podemos construir nossa imagem com o comandodocker build . -t seuuser/blog:latest
. O resultado é uma imagem final com 485MB — cerca de 320MB a mais do que imagem base node-alpine.