Главная / Статьи

Развертывание NestJS на Bun и Prisma 7 в Cloud Run без ошибок сборки

Рабочий пайплайн GitHub Actions для развертывания приложения NestJS с использованием Bun, Prisma 7 и Neon в Cloud Run, а также исправления проблем с Docker и подключением, которые часто мешают командам.

Издатель: REACTAPP.TOP5 мин чтения1259 слов

Источники: Zero to Production: Deploying NestJS with Bun, Prisma 7, and Neon DB on Google Cloud Run

Запуск NestJS на Bun с использованием Prisma 7 и серверлесс-реализации Postgres вроде Neon, а также развертывание на Google Cloud Run через GitHub Actions — всё это происходит мгновенно в любом смысле. Однако такая комбинация ещё довольно нова, поэтому возникающие проблемы мало кто знает, и большинству команд требуется день на устранение ошибок сборки в Docker или проблем с подключением к Prisma перед первым успешным развертыванием. В этом руководстве представлена рабочая схема обработки кода и объясняются способы её реализации, что позволит вам сэкономить этот день.

Как формируется схема обработки

Цель — автоматизированный процесс без участия человека: достаточно загрузить код в ветку для развертывания, и система автоматически выполнит миграции базы данных, соберёт минимальный образ Docker, загрузит его в Artifact Registry и развернёт на серверлесс-сервисе. Порядок действий имеет значение, а некоторые шаги требуют особого подхода из-за особенностей работы Prisma и Bun внутри контейнера.

Удовлетворительная работа генератора Prisma во время сборки в Docker

Контейнеризация Prisma имеет одну постоянную проблему. При запуске команды prisma generate программа требует строку подключения к базе данных, чтобы проанализировать схему и создать клиентский код. Во время сборки нет причин предоставлять ей настоящие учетные данные для производственной среды, а сохранение секретов в слоях образа — это то, чего следует избегать, поскольку любые данные в распространяемом образе могут быть извлечены позже.

Решением является синтаксически корректный, но фиктивный URL, существующий только во время сборки. Приведенный ниже многоэтапный Dockerfile устанавливает зависимости на основе замороженного файла lockfile, задает временную переменную DB__URL, указывающую на несуществующую локальную базу данных, лишь бы генератор работал корректно, собирает приложение и копирует только скомпилированный результат в легкий образ для выполнения.

# Stage 1: Builder
FROM oven/bun:1 AS builder
WORKDIR /app

COPY package.json bun.lock ./
COPY prisma ./prisma
COPY prisma.config.ts ./

RUN bun install --frozen-lockfile

# The "Prisma Satisfaction" Trick:
# Prisma 7 needs to see a URL format during the build phase to generate the client.
ENV DB__URL="postgresql://dummy:dummy@localhost:5432/dummy"

RUN bunx prisma generate

COPY . .

RUN bun run build

# Stage 2: Runner (Slim)
FROM oven/bun:1-slim AS runner
WORKDIR /app

# Only copy what is strictly necessary to keep the image small
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/prisma.config.ts ./

ENV NODE_ENV=production
ENV PORT=8080

CMD ["bun", "run", "dist/main.js"]

Важны два фактора. Многоэтапная сборка выполняется с полной версией образа Bun и работает на bun:1-slim, что позволяет сохранять итоговый образ небольшим и сокращать количество возможных точек атаки. Кроме того, фиктивная строка удовлетворяет требованиям генератора, поскольку никакие настоящие учетные данные не попадают в состав образа — генератору на этапе сборки нужна лишь структура URL к базе данных Postgres, а не доступный сервер.

Ограничение прав учетной записи сервиса

Пайплайн аутентифицируется в Google Cloud с использованием учетной записи сервиса, и бывает соблазн предоставить ей широкие права, чтобы избежать ошибок. Сопротивляйтесь этому. Следуя принципу минимальных привилегий, предоставьте ей только те роли, которые необходимы для развертывания, чтобы утечка ключей не привела к контролю над всем проектом: загрузка образов в Artifact Registry и развертывание в Cloud Run, ничего больше.

Полный процесс работы GitHub Actions

Перед первым запуском сохраняйте значения, на которые ссылается рабочий процесс, такие как ключ учетной записи сервиса и URL базы данных, в виде зашифрованных секретов хранилища. Задача загружает код, настраивает Bun, аутентифицируется в Google Cloud, выполняет миграцию через Prisma, аутентифицирует Docker в Artifact Registry, собирает и загружает образ, развертывает его в Cloud Run и удаляет старые образы. Источник помечает этот фрагмент как SQL; это YAML-рабочий процесс GitHub Actions, и приведенное ниже пояснение правильно его описывает.

name: Deploy Server to Cloud Run

on:
  push:
    branches: ['main']

env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
  REGION: asia-south1
  REPO_NAME: cloud-run-source-deploy
  SERVICE_NAME: server

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v2

      # STEP: Database Migration
      # Run migrations BEFORE deployment to ensure the DB is ready for new code.
      - name: Run Prisma Migrations
        run: |
          bun install --frozen-lockfile
          bunx prisma generate
          bunx prisma migrate deploy
        env:
          DB__URL: ${{ secrets.DB__URL }}

      - name: Docker Auth
        run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev

      - name: Build and Push
        run: |
          IMAGE_URI=${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/${{ env.SERVICE_NAME }}:latest
          docker build -t $IMAGE_URI .
          docker push $IMAGE_URI
          echo "IMAGE_URI=$IMAGE_URI" >> $GITHUB_ENV

      - name: Deploy to Cloud Run
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: ${{ env.SERVICE_NAME }}
          region: ${{ env.REGION }}
          image: ${{ env.IMAGE_URI }}
          flags: '--port=8080 --allow-unauthenticated'
          env_vars: |
            DB__URL=${{ secrets.DB__URL }}
            # Add placeholders for other secrets below:
            # SMTP__HOST=${{ secrets.SMTP__HOST }}
            # AUTH__TOKEN_SECRET=${{ secrets.AUTH__TOKEN_SECRET }}

      # STEP: Housekeeping (Optional)
      # Deletes old image digests to save on storage costs.
      - name: Delete Old Images
        shell: bash
        run: |
          IMAGE_PATH="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/${{ env.SERVICE_NAME }}"
          gcloud artifacts docker images list "$IMAGE_PATH" --sort-by=~create_time > all_images.txt
          awk 'NR>2 {print $2}' all_images.txt > digests_to_delete.txt

          if [ -s digests_to_delete.txt ]; then
            while read digest; do
              if [[ "$digest" == sha256:* ]]; then
                echo "Deleting old image: $digest"
                gcloud artifacts docker images delete "$IMAGE_PATH@$digest" --delete-tags --quiet || true
              fi
            done < digests_to_delete.txt
          fi

Почему эти сложные шаги необходимы

Выполнение команды prisma migrate deploy как отдельного шага перед запуском нового образа гораздо безопаснее, чем миграция изнутри контейнера при его запуске. Если миграция проваливается, процесс развертывания останавливается, и текущая версия продолжает обслуживать трафик; если же миграция выполняется при старте контейнера, сбой может привести к простою сервиса в момент, когда новый код уже начинает работать.

Последний шаг по уборке заключается в удалении старых хэшей образов, поскольку в активной среде накапливаются хранимые файлы, что приводит к росту счетов за хранилище. Этот шаг действительно является необязательным: если вы полагаетесь на сохранение каждой исторической версии для возможности возврата, можете его пропустить и оплатить хранение только необходимых данных.

Последний способ тихого сбоя — исчерпание подключений. Cloud Run быстро масштабирует инстансы, и каждая из них открывает собственные подключения, поэтому указание прямого адреса базы данных позволяет внезапному спику превысить её лимит подключений. В Neon используйте строку подключения с пулом в поле DB__URL, чтобы множество кратковременных инстансов делились ограниченным пулом. Это применимо к любой конфигурации серверлесс-решений с PostgreSQL.

Основные выводы

  • Во время сборки укажите генератору Prisma фиктивный, корректно сформированный URL базы данных, чтобы никакие реальные секреты не попадали в слой изображений.
  • Собирайте проект с полной версией изображения Bun и запускайте его в легковесной версии, чтобы изображения оставались небольшими.
  • Выполняйте миграции отдельным шагом перед развертыванием, чтобы сбой останавливал процесс выпуска, а не нарушал работу активного сервиса.
  • Для серверлесс-целей используйте строку подключения с пулом, а учетную запись сервиса развертывания настраивайте с минимальными привилегиями.
  • На момент написания Bun в стадии разработки и Prisma 7 по-прежнему считаются передовыми решениями; проверьте поведение инструментов согласно актуальной документации.