NestJS auf Bun und Prisma 7 in Cloud Run bereitstellen, ohne Build-Fehler
Ein funktionsfähiger GitHub Actions-Pipeline zum Bereitstellen einer NestJS-Anwendung mit Bun, Prisma 7 und Neon auf Cloud Run, sowie die Docker- und Verbindungsbehebungen, die Teams in Schwierigkeiten bringen.
Quellen: Zero to Production: Deploying NestJS with Bun, Prisma 7, and Neon DB on Google Cloud Run
Das Ausführen von NestJS auf Bun, unterstützt durch Prisma 7 sowie ein serverloses Postgres wie Neon, und dessen Bereitstellung auf Google Cloud Run über GitHub Actions ist in jeder Hinsicht schnell. Das Problem ist, dass diese Kombination noch neu genug ist, sodass die damit verbundenen Fehler unbekannt sind – die meisten Teams verlieren einen Tag wegen Docker-Build-Fehlern oder Prisma-Verbindungsproblemen, bevor es zum ersten erfolgreichen Deployment kommt. Diese Anleitung stellt Ihnen einen funktionierenden Pipeline vor und erklärt die dahinterstehenden Tricks, damit Sie diesen Tag überspringen können.
Wie der Pipeline aufgebaut ist
Ziel ist ein automatisierter Ablauf: Sobald Sie den Code in Ihre Deployment-Branch pushen, führt die Automatisierung die Datenbank-Migrationen durch, erstellt ein kompaktes Docker-Bild, pusht es in das Artifact Registry und setzt es anschließend in einem serverlosen Service ein. Die Reihenfolge der Schritte ist wichtig, und einige Schritte erfordern besondere Behandlung aufgrund des Verhaltens von Prisma und Bun innerhalb eines Containers.
Der Prisma-Generator während des Docker-Builds zufriedenstellen
Die Containerisierung von Prisma weist ein anhaltendes Problem auf. Wenn prisma generate ausgeführt wird, benötigt es eine Datenbankverbindungszeichenkette, um das Schema zu analysieren und den Client zu erzeugen. Während des Builds gibt es keinen Grund, ihm echte Produktionsdaten zur Verfügung zu stellen, und das Einbauen eines Geheimnisses in eine Image-Schicht sollte vermieden werden, da alles in einem distribuierbaren Image später extrahiert werden kann.
Die Lösung besteht darin, eine syntaktisch gültige, aber falsche URL zu verwenden, die nur für den Build existiert. Das untenstehende mehrstufige Dockerfile installiert Abhängigkeiten anhand eines festgefrorenen Lockfiles, setzt eine temporäre DB__URL, die auf eine nicht existierende lokale Datenbank verweist – rein damit der Generator zufrieden ist – baut die Anwendung und kopiert nur das kompilierte Ergebnis in ein schlankes Laufzeitimage.
# 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"]
Zwei Entscheidungen sind hier wichtig. Der mehrstufige Build verwendet das vollständige Bun-Bild und läuft unter bun:1-slim, wodurch das endgültige Bild klein bleibt und die Angriffsfläche für Angreifer eingeschränkt wird. Zudem erfüllt der Dummy-String die Anforderungen des Generators, ohne dass echte Zugangsdaten jemals in eine Schicht gelangen, da der Generator zum Zeitpunkt des Builds nur die Struktur einer Postgres-URL benötigt und nicht einen zugänglichen Server.
Sicherung des Service-Accounts
Die Pipeline authentifiziert sich bei Google Cloud mit einem Service-Account, und es ist verlockend, diesem umfassenden Zugriff zu gewähren, um Fehler zu vermeiden. Widerstehen Sie diesem Drang. Befolgen Sie das Prinzip des geringsten Privilegs und erteilen Sie dem Account nur die Rollen, die für den Deployment-Prozess notwendig sind, damit ein durchgesickertes Passwort nicht zur Kontrolle des gesamten Projekts genutzt werden kann – lediglich das Hochladen von Bildern in den Artifact Registry und das Deployment auf Cloud Run, nichts weiter.
Der GitHub Actions Workflow von Anfang bis Ende
Speichern Sie die vom Workflow verwendeten Werte, wie beispielsweise den Service-Account-Schlüssel und die Datenbank-URL, als verschlüsselte Repository-Geheimnisse vor dem ersten Ausführungsvorgang. Die Aufgabe holt den Code ab, stellt Bun ein, authentifiziert sich bei Google Cloud, führt die Prisma-Migration durch, authentifiziert Docker gegen das Artifact Registry, baut und pusht das Image, deployt es auf Cloud Run und löscht alte Images. Die Quelle kennzeichnet diesen Abschnitt als SQL; es handelt sich um einen GitHub Actions YAML-Workflow, und die untenstehende Anmerkung kennzeichnet ihn korrekt.
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
Warum die schwierigen Schritte ihren Platz haben
Das Ausführen von prisma migrate deploy als eigenständigen Schritt vor dem Go-Live der neuen Image-Version ist weitaus sicherer als die Migration direkt aus dem Container beim Starten. Fällt eine Migration im Pipeline-Prozess fehl, stoppt das Deployen und die aktuelle Version weiterhin den Traffic; führt man hingegen die Migration beim Booten des Containers durch, kann eine fehlerhafte Migration den Service lahmlegen, während der neue Code bereits gestartet wird.
Der abschließende Aufräumschritt löscht alte Image-Digests, da sich in einem stark genutzten Umfeld gespeicherte Artefakte anhäufen und die Speicherkosten steigen. Dies ist tatsächlich optional: Wenn Sie darauf vertrauen, jederzeit auf eine historische Build-Version zurückgreifen zu können, können Sie diesen Schritt weglassen und die Speicherkosten tragen.
Der letzte stille Ausfallmodus ist die Erschöpfung der Verbindungen. Cloud Run skaliert Instanzen schnell auf, und jede öffnet eigene Verbindungen – dadurch kann ein plötzlicher Anstieg die Verbindungsbeschränkung einer Datenbank überschreiten, wenn man auf die direkte Adresse der Datenbank verweist. Bei Neon sollte man den gepoolten Verbindungsstring in DB__URL verwenden, damit viele kurzlebige Instanzen einen begrenzten Pool teilen. Dies gilt für jede Konfiguration von serverless zu Postgres.
Kernpunkte
- Geben Sie dem Generator von Prisma zur Zeit der Erstellung eine falsche, korrekt formatierte Datenbank-URL an, damit keine echten Geheimnisse in einer Image-Schicht landen.
- Bauen Sie mit dem vollständigen Bun-Image und führen Sie die Anwendung mit der kompakten Variante aus, um die Größe der Images zu halten.
- Führen Sie Migrationsvorgänge als eigenständigen Schritt vor dem Deploy aus, damit ein Fehler die Veröffentlichung stoppt und kein laufender Dienst beeinträchtigt wird.
- Verwenden Sie einen gepoolten Verbindungsstring für serverless-Ziele und beschränken Sie das Service-Konto des Deploys auf das notwendigste Privileg.