Déployer NestJS sur Bun et Prisma 7 dans Cloud Run sans erreurs de compilation
Un pipeline GitHub Actions fonctionnel pour déployer une application NestJS sur Bun avec Prisma 7 et Neon vers Cloud Run, ainsi que les corrections relatives à Docker et aux connexions qui posent problème aux équipes.
Sources: Zero to Production: Deploying NestJS with Bun, Prisma 7, and Neon DB on Google Cloud Run
Exécuter NestJS sur Bun, avec le soutien de Prisma 7 et d’un Postgres sans serveur comme Neon, puis le déployer sur Google Cloud Run via GitHub Actions est rapide à tous égards. Le problème, c’est que cette combinaison est suffisamment nouvelle pour que ses pannes soient peu connues ; la plupart des équipes perdent une journée à cause d’erreurs de construction avec Docker ou de problèmes de connexion Prisma avant le premier déploiement réussi. Cette démarche vous fournit un pipeline fonctionnel et explique les astuces qui le rendent efficace, afin que vous puissiez éviter cette journée perdue.
Comment est structuré le pipeline
L’objectif est un flux automatisé : il suffit de pousser les modifications vers votre branche de déploiement, et l’automatisation exécute vos migrations de base de données, crée une image Docker légère, la met à jour dans Artifact Registry, puis la déploie sur un service sans serveur. L’ordre des étapes est important, et certaines nécessitent un traitement particulier en raison du comportement de Prisma et de Bun à l’intérieur d’un conteneur.
Satisfaire le générateur de Prisma pendant la construction Docker
La conteneurisation de Prisma présente un problème persistant. Lorsque prisma generate est exécuté, il nécessite une chaîne de connexion à la base de données afin de pouvoir analyser le schéma et générer le client. Pendant une construction, il n’y a aucune raison de lui fournir des identifiants réels destinés à la production, et intégrer un secret dans une couche d’image est quelque chose à éviter, car tout ce qui se trouve dans une image distribuable peut être extrait par la suite.
La solution consiste à utiliser une URL syntaxiquement valide mais fictive qui n’existe que pendant la construction. Le Dockerfile à plusieurs étapes ci-dessous installe les dépendances en se basant sur un fichier lockfile figé, définit une variable DB__URL temporaire pointant vers une base de données locale inexistante uniquement pour satisfaire le générateur, construit l’application, puis ne copie que les résultats compilés dans une image de runtime légère.
# 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"]
Deux décisions sont importantes ici. La construction en plusieurs étapes utilise l’image complète Bun et s’exécute sur bun:1-slim, ce qui permet de garder l’image finale petite et de réduire les surfaces exploitables par un attaquant. De plus, la chaîne de caractères fictive satisfait le générateur sans que des identifiants réels n’entrent jamais dans une couche, car le générateur a besoin uniquement de la structure d’une URL Postgres au moment de la construction, et non d’un serveur accessible.
Sécuriser le compte de service
Le pipeline s’authentifie auprès de Google Cloud à l’aide d’un compte de service, et il est tentant d’accorder des droits étendus pour éviter les erreurs. Résistez à cette tentation. En suivant le principe du moindre privilège, accordez-lui uniquement les rôles nécessaires au déploiement, afin qu’une clé divulguée ne puisse pas être utilisée pour prendre le contrôle de l’ensemble du projet : uniquement le dépôt d’images dans Artifact Registry et le déploiement sur Cloud Run, rien de plus.
Le flux GitHub Actions du début à la fin
Avant la première exécution, stockez les valeurs auxquelles fait référence le flux de travail, telles que la clé du compte de service et l’URL de la base de données, en tant que secrets chiffrés dans le répertoire. La tâche télécharge le code, configure Bun, s’authentifie auprès de Google Cloud, exécute la migration Prisma, authentifie Docker auprès d’Artifact Registry, construit et déploie l’image sur Cloud Run, puis supprime les anciennes images. Les étiquettes indiquent ce fragment comme SQL ; il s’agit en réalité d’un flux de travail YAML GitHub Actions, et la note ci-dessous le désigne correctement.
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
Pourquoi ces étapes complexes ont leur place
Exécuter prisma migrate deploy en tant que étape distincte, avant que la nouvelle image ne soit mise en ligne, est bien plus sûr que de procéder à la migration depuis l’intérieur du conteneur au démarrage. Si une migration échoue dans le pipeline, le déploiement s’arrête et la version actuelle continue de gérer le trafic ; en revanche, si la migration a lieu au démarrage du conteneur, une mauvaise migration peut faire cesser le service alors que le nouveau code est déjà en train de s’exécuter.
L’étape finale de nettoyage consiste à supprimer les anciens digests d’image, car dans un environnement chargé, les artefacts stockés s’accumulent et la facture de stockage augmente. C’est vraiment optionnel : si vous comptez sur le fait de conserver chaque version historique pour pouvoir faire un rollback, vous pouvez omettre cette étape et assumer le coût de stockage.
Le dernier mode de défaillance silencieux est l’épuisement des connexions. Cloud Run augmente rapidement le nombre d’instances, et chacune ouvre ses propres connexions ; par conséquent, en utilisant l’adresse directe d’une base de données, une forte charge peut dépasser sa limite de connexions. Sur Neon, utilisez la chaîne de connexion partagée dans DB__URL afin que de nombreuses instances à durée de vie courte partagent un pool limité. Cela s’applique à toute configuration serverless vers Postgres.
Points clés
- Fournissez au générateur de Prisma une URL de base de données fictive et bien formatée lors de la compilation, afin qu’aucun secret réel ne se retrouve dans une couche d’image.
- Compilez avec l’image complète de Bun et exécutez sur la version allégée pour conserver de petites tailles d’images.
- Exécutez les migrations en tant qu’étape distincte avant le déploiement, afin qu’une défaillance arrête la publication sans perturber un service en ligne.
- Utilisez une chaîne de connexion partagée pour les cibles serverless et restreignez le compte de service de déploiement aux droits nécessaires uniquement.