5 шагов для докеризации NestJ с помощью Prisma
7 февраля 2023 г.В этой статье мы покажем, как докеризировать приложение NestJS + Prisma.
Мы выходим за рамки основ и следуем рекомендациям из Dockerfiles и Snyk. Наш окончательный Dockerfile
выглядит так:
FROM node:18 as build
WORKDIR /usr/src/app
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN npx prisma generate
RUN npm run build
FROM node:18-slim
RUN apt update && apt install libssl-dev dumb-init -y --no-install-recommends
WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app/dist ./dist
COPY --chown=node:node --from=build /usr/src/app/.env .env
COPY --chown=node:node --from=build /usr/src/app/package.json .
COPY --chown=node:node --from=build /usr/src/app/package-lock.json .
RUN npm install --omit=dev
COPY --chown=node:node --from=build /usr/src/app/node_modules/.prisma/client ./node_modules/.prisma/client
ENV NODE_ENV production
EXPOSE 3000
CMD ["dumb-init", "node", "dist/src/main"]
Теперь давайте начнем с основ и улучшим наш Dockerfile.
1. Базовый файл Docker
Простой стартовый файл dockerfile, который мы можем создать, состоит из следующего:
FROM node # Use basic node image
WORKDIR /usr/src/app # Set working dir inside base docker image
COPY . . # Copy our project files to docker image
RUN npm install # Install project dependencies
RUN npx prisma generate # Generate Prisma client files
RUN yarn build # Build our nestjs
EXPOSE 3000 # Espose our app port for incoming requests
CMD ["npm", "run","start:prod"] # Run our app
Предыдущий Dockerfile генерировал правильный образ и не было проблем. Он будет использовать последний образ узла (Node v18 на момент написания статьи) и запустит наше приложение.
2. Добавление .dockerignore в наш проект
Мы хотим быть уверены, что никакие конфиденциальные файлы не передаются в наш образ Docker. В частности, во время команды COPY . .код>. Если мы запускаем это из конвейера CI, это только что загрузил репозиторий. Надеюсь, мы соблюдаем осторожность и в git не просочились какие-либо учетные данные. Так что никаких последствий для нашего docker-образа нет.
Но во время установки у нас могут быть более сложные рабочие процессы и создание некоторых конфиденциальных файлов. Либо так, либо мы создаем наш образ локально, чтобы поделиться им или протестировать его. Лучше убедиться, что у нас есть файл .dockerignore
, предотвращающий это.
Это может быть что-то вроде этого:
.dockerignore # Ignore the ignore
node_modules # Ignore local node_modules folder
npm-debug.log # Debug files
Dockerfile # The dockerfile
.git # The git history
.gitignore #
.npmrc # If accessing a private npm repository here will be the token used, so ignore to prevent leaking
.env-* # Any other environment that we don't want to include
.gitlab-* # Deploying with gitlab ?
.github # Using github actions ?
*.md # Any
3 Создание многоэтапного изображения
Оптимизируя наш образ, мы можем извлечь выгоду из использования многоэтапного образа Docker, уменьшив размер образа (что стоит нам денег в некоторых средах, таких как AWS ECR, а также экономит нам пропускную способность и время при развертывании).
Мы можем установить наши пакеты и собрать приложение за один шаг. Затем используйте другой образ, скопируйте сгенерированные файлы сборки и установите только рабочие зависимости. На этапе сборки мы могли бы продолжить использовать образ базового узла, который использовался до сих пор. В идеале мы хотим использовать одну и ту же версию инструментов на всех этапах, одну и ту же версию узла, базовую ОС и пакеты. Мы могли бы продолжить перечисление всех деталей, используя определенный тег изображения, такой как 14.21.2-buster
, или не использовать какой-либо конкретный тег, который по умолчанию будет использовать latest
, как мы сделали на представлен первый Dockerfile.
Я бы порекомендовал, по крайней мере, указать основную версию узла, которую вы используете, что даст нам образ, который, как мы знаем, в основном совместим с нашей локальной средой, но также будет использовать самый последний официальный образ, уменьшая постоянно обнаруживаемые уязвимости и ошибки.
Пока что мы можем изменить первые шаги нашего Dockerfile:
FROM node:18 as build # Naming our image to be use in later steps
WORKDIR /usr/src/app
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN npx prisma generate
RUN npm run build
На следующем шаге, чтобы уменьшить размер окончательного изображения, мы хотели бы использовать уменьшенное базовое изображение, такое как slim
или alpine
(хотя alpine
имеет меньшего размера, он не создан с помощью libc
, и некоторые инструменты могут работать не так, как ожидалось, будьте осторожны). Что может случиться (и происходит в нашем случае с использованием Prisma), так это то, что этот меньший slim
не содержит некоторых библиотек или инструментов, необходимых нашему приложению. В этом случае нам нужно добавить libssl-dev
.
Мы также должны установить для переменной de NODE_ENV
значение production
, чтобы различные модули работали соответствующим образом, уменьшая нагрузку отладочных символов и журналов.
FROM node:18-slim # Base smaller node image
RUN apt update && apt install libssl-dev -y --no-install-recommends # Add missing dependency needed for prisma
WORKDIR /usr/src/app
COPY --from=build /usr/src/app/dist ./dist # Copy de dist folder generated in the previous step
COPY --from=build /usr/src/app/.env .env # Copy env variables to use
COPY --from=build /usr/src/app/package.json .
COPY --from=build /usr/src/app/package-lock.json .
RUN npm install --omit=dev # Install without dev dependencies to save some space
COPY --from=build /usr/src/app/node_modules/.prisma/client ./node_modules/.prisma/client # Copy generated prisma client from previous step
ENV NODE_ENV production
EXPOSE 3000
CMD ["npm", "run","start:prod"]
С помощью этих двух шагов мы сэкономим около 300 МБ только для разных базовых изображений.
Небольшое примечание: в старых версиях npm
используйте npm install --production
4 Улучшенный запуск приложения
Есть несколько предостережений при запуске нашего приложения напрямую через npm
. Во-первых, npm не пересылает никаких сигналов порожденному процессу, и нашему процессу будет присвоен PID 1, который по-разному обрабатывается ядром нашего образа докера. Это может повлиять на возможность корректного закрытия нашего приложения и вызвать проблемы с отладкой.
Для получения дополнительной информации см. ссылки в конце.
Итак, давайте изменим нашу команду RUN
на:
CMD ["dumb-init", "node", "dist/src/main"]
5 Безопасность
Мы никогда не должны запускать наше приложение с привилегиями root, и хотя текущие образы node запускаются по умолчанию с пользователем с низкими привилегиями node
, все файлы, которые мы скопировали, принадлежат пользователю root. .
В некоторых облачных средах, таких как AWS или Azure, это может иметь практически нулевые последствия, лучше не рисковать. Таким образом, в каждой копии мы будем переходить на одного и того же пользователя node
. Затем мы могли бы добавить аргумент --chown=node:node
ко всем нашим командам COPY
.
Собрав все вместе, мы получим Dockerfile
, который мы представили в начале:
FROM node:18 as build
WORKDIR /usr/src/app
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN npx prisma generate
RUN npm run build
FROM node:18-slim
RUN apt update && apt install libssl-dev dumb-init -y --no-install-recommends
WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app/dist ./dist
COPY --chown=node:node --from=build /usr/src/app/.env .env
COPY --chown=node:node --from=build /usr/src/app/package.json .
COPY --chown=node:node --from=build /usr/src/app/package-lock.json .
RUN npm install --omit=dev
COPY --chown=node:node --from=build /usr/src/app/node_modules/.prisma/client ./node_modules/.prisma/client
ENV NODE_ENV production
EXPOSE 3000
CMD ["dumb-init", "node", "dist/src/main"]
Если вы хотите углубиться, я рекомендую вам следующие статьи:
- Snyk: 10 рекомендаций по контейнеризации Node.js веб-приложения с Docker
- Docker: рекомендации по созданию файлов Docker
- Sysdig: 20 лучших практик использования Dockerfile
- Yelp: Представляем тупую систему инициализации для контейнеров Docker.
Оригинал