Aller au contenu

Note d'ingénierie

NEXT_PUBLIC_ : la variable vide qui casse un déploiement sans lever d'erreur

Par Alexandre Kaczor4 min de lecture
  • Next.js
  • Docker
  • Déploiement
  • DevOps
Illustration de l'article « NEXT_PUBLIC_ : la variable vide qui casse un déploiement sans lever d'erreur »

Un déploiement « réussi », un site en ligne et un formulaire de vente mort : une variable de build absente, inlinée à vide, sans erreur. Le piège et sa parade.

Le déploiement passe. Les conteneurs sont healthy. Le site répond en 200. Et pourtant le formulaire de devis ne part plus, le captcha est invisible, et les URLs canoniques pointent vers /contact au lieu de https://mondomaine.fr/contact.

Ce scénario m'est arrivé en production. Il n'a produit aucune erreur, aucun log rouge, aucune alerte. C'est ce qui le rend redoutable.

Ce qu'est réellement une variable NEXT_PUBLIC_

Il faut se débarrasser d'une intuition fausse : une variable préfixée NEXT_PUBLIC_ — comme VITE_ chez Vite ou PUBLIC_ chez SvelteKit — n'est pas lue à l'exécution. Elle est remplacée textuellement dans le code source au moment de la construction.

Concrètement, ce code :

const apiUrl = process.env.NEXT_PUBLIC_API_URL;

ne devient pas « lis la variable d'environnement au démarrage ». Il devient, littéralement, dans le bundle JavaScript envoyé au navigateur :

const apiUrl = "https://api.mondomaine.fr";

La substitution a lieu pendant next build. Après ça, la variable n'existe plus. Elle a disparu du programme.

Le piège, en deux temps

1. La déclarer dans env_file ne sert à rien

C'est l'erreur la plus fréquente sur une pile Docker. On écrit :

services:
  web:
    build: ./apps/web
    env_file: .env.prod        # contient NEXT_PUBLIC_API_URL=...

env_file injecte des variables dans le conteneur au démarrage. Or la substitution a eu lieu bien avant, pendant le build, dans un contexte qui n'a jamais vu ce fichier. La variable arrive après la bataille.

2. Une variable absente est inlinée à vide, en silence

C'est la seconde moitié du piège, et la plus coûteuse. Quand la variable est absente au build, le remplacement a quand même lieu — par la chaîne vide :

const apiUrl = "";

Aucun avertissement. Le build réussit. Le site se déploie. Et le code qui appelle fetch(`${apiUrl}/api/v1/leads`) tape sur /api/v1/leads — sur le domaine du front, où il n'y a rien.

Le piège jumeau, côté code

La parade réflexe est un opérateur de coalescence :

const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "https://api.mondomaine.fr";

Elle ne fonctionne pas. ?? ne se déclenche que sur null et undefined. Une chaîne vide est une valeur parfaitement définie : elle traverse l'opérateur sans être remplacée. Le repli n'est jamais atteint.

|| attraperait bien la chaîne vide, mais masquerait le problème au lieu de le signaler — et introduirait ses propres surprises sur les valeurs numériques ou booléennes.

La parade : faire échouer le build

La bonne réponse n'est pas de rattraper la variable manquante à l'exécution. C'est de refuser de construire une image cassée. Docker Compose sait le faire nativement, avec la syntaxe :? :

services:
  web:
    build:
      context: ./apps/web
      args:
        NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:?variable requise au build}
        NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:?variable requise au build}

Et côté Dockerfile, il faut relayer l'argument en variable d'environnement avant le build :

ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
RUN pnpm build

Désormais, si la variable manque, le déploiement s'arrête avec un message explicite. C'est infiniment préférable à un site en ligne dont l'outil de vente est mort.

Deux fichiers d'environnement, deux règles opposées

Sur une pile Compose, il y a en réalité deux fichiers qui ne servent pas à la même chose, et dont les règles d'échappement sont contraires :

  • .env — lu par Compose lui-même pour interpoler les ${...} du fichier YAML. Règle : aucune valeur contenant un $, elle serait interprétée comme une interpolation.
  • .env.prod — passé aux conteneurs via env_file. Règle : échapper chaque $ en $$.

Un mot de passe SMTP ou de base de données contenant un $ placé dans le mauvais fichier sera tronqué silencieusement. Le symptôme n'apparaît pas au déploiement, mais à la première authentification — souvent des heures plus tard, quand plus personne ne fait le lien.

La vérification qui prend trente secondes

Après le déploiement, ne pas se contenter d'un code 200 sur la page d'accueil. Chercher la valeur dans le bundle réellement servi :

curl -s https://mondomaine.fr/ | grep -o 'https://api\.[a-z.]*' | head -1

Si rien ne remonte, la variable est vide dans l'image. Le site est en ligne, et il est cassé.

Ce que ça dit d'un déploiement en général

Ce piège est un cas particulier d'une règle plus large : un déploiement se prouve par des chiffres, pas par « c'est en ligne ». Un code HTTP, un état healthy, une valeur retrouvée dans le bundle, une requête métier qui aboutit de bout en bout. « Ça a déployé » n'est pas une preuve — c'est une impression.