Inicio / Artículos

Desplegando NestJS en Bun y Prisma 7 en Cloud Run sin errores de compilación

Un pipeline funcional de GitHub Actions para desplegar una aplicación NestJS en Bun con Prisma 7 y Neon en Cloud Run, además de las correcciones para Docker y las conexiones que suelen causar problemas a los equipos.

Editor: REACTAPP.TOP5 min de lectura1259 palabras

Fuentes: Zero to Production: Deploying NestJS with Bun, Prisma 7, and Neon DB on Google Cloud Run

Ejecutar NestJS en Bun, respaldado por Prisma 7 y un Postgres sin servidor como Neon, y enviarlo a Google Cloud Run mediante GitHub Actions es rápido en todos los sentidos. El problema es que esta combinación es lo suficientemente nueva como para que sus fallos sean poco conocidos, y la mayoría de los equipos pierden un día debido a errores en la construcción con Docker o problemas de conexión con Prisma antes del primer despliegue exitoso. Esta guía le proporciona un pipeline funcional y explica los trucos detrás de él, para que pueda ahorrarse ese día.

Cómo se estructura el pipeline

El objetivo es un flujo automático: al hacer push a la rama de despliegue, la automatización ejecuta las migraciones de la base de datos, crea una imagen Docker ligera, la envía al Artifact Registry y la despliega en un servicio sin servidor. El orden de los pasos es importante, y algunos requieren un manejo especial debido a la forma en que Prisma y Bun funcionan dentro de un contenedor.

Satisfacer al generador de Prisma durante la construcción con Docker

La contenerización de Prisma presenta un problema persistente. Cuando se ejecuta prisma generate, este necesita una cadena de conexión a la base de datos para poder analizar el esquema y generar el cliente. Durante el proceso de construcción no hay razón para proporcionarle credenciales reales de producción, y es mejor evitar incluir secretos en las capas de la imagen, ya que cualquier dato contenido en una imagen distribuible puede ser extraído posteriormente.

La solución es utilizar una URL sintácticamente válida pero falsa que exista únicamente durante la construcción. El Dockerfile de múltiples etapas mostrado a continuación instala las dependencias a partir de un archivo lockfile fijo, establece una variable DB__URL temporal que apunta a una base de datos local inexistente, solo para satisfacer al generador, construye la aplicación y copia únicamente los resultados compilados a una imagen de ejecución ligera.

# 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"]

Dos decisiones son cruciales aquí. La construcción en fases múltiples utiliza la imagen completa de Bun y funciona con bun:1-slim, lo que mantiene la imagen final pequeña y reduce las posibilidades de ataque. Además, la cadena ficticia satisface al generador sin que ninguna credencial real ingrese a las capas, ya que el generador solo necesita la estructura de una URL de Postgres en el momento de la construcción, no un servidor accesible.

Seguridad de la cuenta de servicio

El flujo de trabajo se autentica en Google Cloud mediante una cuenta de servicio, y es tentador otorgarle acceso amplio para evitar errores. Hay que resistirse a eso. Siguiendo el principio de mínimo privilegio, solo se le deben asignar los roles necesarios para el despliegue, de modo que una clave filtrada no pueda usarse para tomar control del proyecto completo: solo subir imágenes al Artifact Registry y desplegarlas en Cloud Run, nada más.

El flujo de trabajo de GitHub Actions de principio a fin

Almacene los valores a los que hace referencia el flujo de trabajo, como la clave de la cuenta de servicio y la URL de la base de datos, como secretos encriptados del repositorio antes de la primera ejecución. La tarea descarga el código, configura Bun, se autentica en Google Cloud, ejecuta la migración de Prisma, autentica a Docker frente al Artifact Registry, construye e envía la imagen, la despliega en Cloud Run y elimina las imágenes antiguas. Las etiquetas de origen clasifican este fragmento como SQL; se trata de un flujo de trabajo YAML de GitHub Actions, y la indicación a continuación lo etiqueta correctamente.

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

Por qué los pasos complicados tienen su lugar

Ejecutar prisma migrate deploy como paso independiente, antes de que la nueva imagen entre en producción, es mucho más seguro que realizar la migración desde dentro del contenedor al iniciarlo. Si una migración falla en el proceso, la implementación se detiene y la versión actual sigue atendiendo las solicitudes; si en cambio se realiza la migración al arrancar el contenedor, una migración defectuosa podría interrumpir el servicio mientras ya se está iniciando el nuevo código.

El último paso de mantenimiento elimina los hashes antiguos de las imágenes, ya que en un entorno con mucho tráfico los artefactos almacenados se acumulan y el costo de almacenamiento aumenta. Realmente es opcional: si depende de conservar cada versión anterior para poder hacer un rollback, puede prescindir de este paso y asumir el costo del almacenamiento.

El último modo de fallo silencioso es el agotamiento de conexiones. Cloud Run escala las instancias rápidamente y cada una abre sus propias conexiones, por lo que indicar la dirección directa de una base de datos permite que un pico exceda su límite de conexiones. En Neon, utilice la cadena de conexión compartida en DB__URL para que muchas instancias de corta duración compartan un conjunto limitado de conexiones. Esto aplica a cualquier configuración de tipo serverless hacia Postgres.

Puntos clave

  • Proporcione al generador de Prisma una URL de base de datos ficticia y bien formada durante la fase de construcción, para que ningún secreto real quede almacenado en la capa de imágenes.
  • Construya con la imagen completa de Bun y ejecute la versión ligera para mantener las imágenes pequeñas.
  • Ejecute las migraciones como un paso separado antes del despliegue, de modo que un fallo detenga el lanzamiento en lugar de dañar un servicio en funcionamiento.
  • Utilice una cadena de conexión compartida para los destinos serverless y limite las permisos de la cuenta de servicio de despliegue al mínimo necesario.
  • Bun en producción y Prisma 7 siguen siendo tecnologías de vanguardia al momento de escribir este texto; verifique nuevamente el comportamiento de la herramienta según la documentación actual.
  • Lecturas relacionadas