Cours Next.js · Module 8: Déploiement et production
Variables d'environnement et configuration de production
Dans cette leçon8
Dans la Métropole Quantum 2150, chaque système requiert une configuration environnementale précise. Tout comme, dans la ville cyberpunk, chaque district a ses propres protocoles de sécurité, une application Next.js a besoin de réglages différents pour différents environnements - développement, staging et production. Les variables d'environnement sont la clé d'une gestion de configuration sécurisée et flexible.
Que sont les variables d'environnement ?
Les variables d'environnement sont des paires clé-valeur qui configurent le comportement de l'application sans modifier le code source. Grâce à elles, tu peux stocker :
- Des clés API et des secrets (par ex. des clés de base de données, des tokens d'autorisation)
- Des URL de services externes
- Des flags de configuration (par ex. mode debug, niveau de logs)
- Des réglages spécifiques à l'environnement (développement vs production)
Les fichiers .env dans Next.js
Next.js prend en charge nativement les fichiers .env. Le framework charge automatiquement les variables d'environnement depuis plusieurs fichiers, selon un ordre de priorité précis :
Hiérarchie des fichiers .env
1# Fichiers .env dans Next.js (de la plus haute priorité) :
2.env.local # Surcharges locales - ne JAMAIS commiter dans git !
3.env.development # Uniquement pour npm run dev
4.env.production # Uniquement pour npm run build / npm start
5.env # Valeurs par défaut pour tous les environnementsChaque fichier peut surcharger les valeurs des fichiers de plus basse priorité. Le fichier .env.local a toujours la plus haute priorité et doit figurer dans .gitignore.
Créer des fichiers .env
1# .env - valeurs par défaut (committées dans le repo)
2NEXT_PUBLIC_APP_NAME=QuantumCity
3NEXT_PUBLIC_API_URL=http://localhost:4000/api
4DATABASE_URL=mongodb://localhost:27017/quantum_dev
5JWT_SECRET=development-secret-key-change-in-production
6
7# .env.local - secrets locaux (NE PAS commiter !)
8DATABASE_URL=mongodb://user:secret@prod-cluster.mongodb.net/quantum
9JWT_SECRET=super-cle-secrete-de-production-2150
10OPENAI_API_KEY=sk-proj-abc123...
11STRIPE_SECRET_KEY=sk_live_abc123...
12
13# .env.production - réglages de production
14NEXT_PUBLIC_API_URL=https://api.quantum-city.com
15NEXT_PUBLIC_APP_NAME=QuantumCity Production
16NODE_ENV=productionLe préfixe NEXT_PUBLIC_ - la clé de la sécurité
C'est l'une des règles les plus importantes dans Next.js ! Le préfixe NEXT_PUBLIC_ détermine si la variable est disponible dans le navigateur :
1// Variables SANS préfixe - disponibles UNIQUEMENT sur le serveur
2// Elles n'arrivent jamais dans le bundle du navigateur !
3const dbUrl = process.env.DATABASE_URL; // OK dans les Server Components
4const apiKey = process.env.OPENAI_API_KEY; // OK dans les Route Handlers
5const secret = process.env.JWT_SECRET; // OK dans le middleware
6
7// Variables AVEC le préfixe NEXT_PUBLIC_ - disponibles PARTOUT
8// Elles arrivent dans le bundle JavaScript du navigateur !
9const appName = process.env.NEXT_PUBLIC_APP_NAME; // OK dans les Client Components
10const apiUrl = process.env.NEXT_PUBLIC_API_URL; // OK partoutExemple d'utilisation dans les composants
1// app/layout.tsx - Server Component
2export default function RootLayout({ children }: { children: React.ReactNode }) {
3 // Sûr - variable serveur, n'arrive pas dans le navigateur
4 const dbUrl = process.env.DATABASE_URL;
5 console.log('Connecting to:', dbUrl);
6
7 return (
8 <html>
9 <body>
10 {/* NEXT_PUBLIC_ - utilisé en toute sécurité dans le HTML */}
11 <h1>{process.env.NEXT_PUBLIC_APP_NAME}</h1>
12 {children}
13 </body>
14 </html>
15 );
16}
17
18// components/ApiStatus.tsx - Client Component
19'use client';
20import { useEffect, useState } from 'react';
21
22export default function ApiStatus() {
23 const [status, setStatus] = useState('checking...');
24
25 useEffect(() => {
26 // Les variables NEXT_PUBLIC_ fonctionnent dans le navigateur
27 const apiUrl = process.env.NEXT_PUBLIC_API_URL;
28
29 fetch(apiUrl + '/health')
30 .then(res => res.json())
31 .then(data => setStatus(data.status));
32 }, []);
33
34 return <span>API: {status}</span>;
35}Variables d'environnement Runtime vs Build-time
Il est important de comprendre à quel moment les variables sont lues :
Build-time (au moment du build)
Les variables NEXT_PUBLIC_ sont injectées pendant npm run build. Cela signifie qu'après le build de l'application, leurs valeurs sont « figées » dans le code JavaScript :
1# Ces valeurs seront injectées dans le bundle pendant le build :
2NEXT_PUBLIC_API_URL=https://api.quantum-city.com npm run build
3
4# Après le build, modifier cette variable NE change PAS le comportement de l'application !
5# Tu dois rebuilder pour appliquer une nouvelle valeur.Runtime (au moment de l'exécution)
Les variables serveur (sans NEXT_PUBLIC_) sont lues en temps réel - à chaque requête :
1// app/api/config/route.ts
2import { NextResponse } from 'next/server';
3
4export async function GET() {
5 // Cette valeur est lue À CHAQUE requête
6 // Modifier la variable sur le serveur affecte immédiatement la réponse
7 const dbUrl = process.env.DATABASE_URL;
8 const maxConnections = process.env.DB_MAX_CONNECTIONS || '10';
9
10 return NextResponse.json({
11 database: dbUrl ? 'connected' : 'not configured',
12 maxConnections: parseInt(maxConnections),
13 });
14}Validation des variables d'environnement avec Zod (pattern t3-env)
Dans les applications professionnelles, il vaut la peine de valider les variables d'environnement au démarrage de l'application pour éviter les erreurs au runtime. Une approche populaire consiste à utiliser la bibliothèque Zod :
1// lib/env.ts
2import { z } from 'zod';
3
4// Schéma de validation des variables serveur
5const serverEnvSchema = z.object({
6 DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
7 JWT_SECRET: z.string().min(32, 'JWT_SECRET must have at least 32 characters'),
8 OPENAI_API_KEY: z.string().startsWith('sk-', 'Invalid OpenAI key'),
9 NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
10 PORT: z.coerce.number().default(4000),
11});
12
13// Schéma de validation des variables client
14const clientEnvSchema = z.object({
15 NEXT_PUBLIC_APP_NAME: z.string().min(1),
16 NEXT_PUBLIC_API_URL: z.string().url(),
17});
18
19// Validation à l'import du module
20const serverEnv = serverEnvSchema.safeParse(process.env);
21const clientEnv = clientEnvSchema.safeParse(process.env);
22
23if (!serverEnv.success) {
24 console.error('Missing server variables:', serverEnv.error.format());
25 throw new Error('Invalid server environment configuration');
26}
27
28if (!clientEnv.success) {
29 console.error('Missing client variables:', clientEnv.error.format());
30 throw new Error('Invalid client environment configuration');
31}
32
33export const env = {
34 ...serverEnv.data,
35 ...clientEnv.data,
36};Utiliser des variables validées
1// app/api/users/route.ts
2import { env } from '@/lib/env';
3
4export async function GET() {
5 // TypeScript connaît les types - env.DATABASE_URL est toujours une string
6 // La validation garantit que la valeur est une URL valide
7 const response = await fetch(env.NEXT_PUBLIC_API_URL + '/users', {
8 headers: {
9 'Authorization': 'Bearer ' + env.JWT_SECRET,
10 },
11 });
12
13 return Response.json(await response.json());
14}Configuration de next.config.js pour différents environnements
Le fichier next.config.js peut utiliser des variables d'environnement pour une configuration dynamique :
1// next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4 // Définition de variables d'environnement supplémentaires
5 env: {
6 CUSTOM_KEY: process.env.NODE_ENV === 'production'
7 ? 'production-value'
8 : 'development-value',
9 BUILD_TIME: new Date().toISOString(),
10 },
11
12 // Configuration des images dépendante de l'environnement
13 images: {
14 remotePatterns: [
15 {
16 protocol: 'https',
17 hostname: process.env.NODE_ENV === 'production'
18 ? 'cdn.quantum-city.com'
19 : 'localhost',
20 },
21 ],
22 },
23
24 // En-têtes différents pour dev et prod
25 async headers() {
26 const securityHeaders = [
27 { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
28 { key: 'X-Content-Type-Options', value: 'nosniff' },
29 ];
30
31 if (process.env.NODE_ENV === 'production') {
32 securityHeaders.push({
33 key: 'Strict-Transport-Security',
34 value: 'max-age=63072000; includeSubDomains; preload',
35 });
36 }
37
38 return [{ source: '/:path*', headers: securityHeaders }];
39 },
40};
41
42module.exports = nextConfig;Gérer les secrets en production
Dans l'environnement de production, nous ne stockons jamais les secrets dans des fichiers .env. À la place, nous utilisons des plateformes de gestion de secrets :
1# Vercel - ajout de variables via la CLI
2vercel env add DATABASE_URL production
3vercel env add JWT_SECRET production
4vercel env add OPENAI_API_KEY production
5
6# Vercel - aperçu des variables
7vercel env ls
8
9# Docker - variables via docker-compose.yml
10# docker-compose.yml:
11# services:
12# app:
13# environment:
14# - DATABASE_URL=${DATABASE_URL}
15# - JWT_SECRET=${JWT_SECRET}
16
17# GitHub Actions - secrets dans la CI/CD
18# Settings → Secrets and variables → Actions
19# Utilisation : ${{ secrets.DATABASE_URL }}Résumé
Les variables d'environnement sont le fondement d'une configuration sécurisée d'une application Next.js :
- Fichiers .env - hiérarchie :
.env.local>.env.production>.env - Préfixe NEXT_PUBLIC_ - seules ces variables arrivent dans le navigateur
- Runtime vs Build-time - les variables serveur sont dynamiques, les variables client sont figées après le build
- Validation avec Zod - garantit la justesse de la configuration
- Secrets en production - jamais dans des fichiers, toujours via la plateforme d'hébergement
Entraîne-toi à ces concepts dans l'éditeur ci-dessous.
Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Demo: Environment variables and production configuration
4interface EnvVar {
5 name: string;
6 value: string;
7 type: 'server' | 'public';
8 file: string;
9 description: string;
10}
11
12const envVars: EnvVar[] = [
13 { name: 'DATABASE_URL', value: 'mongodb://localhost:27017/quantum', type: 'server', file: '.env', description: 'Database URL' },
14 { name: 'JWT_SECRET', value: 'dev-secret-key-change-me', type: 'server', file: '.env.local', description: 'JWT key (secret!)' },
15 { name: 'OPENAI_API_KEY', value: 'sk-proj-abc123...', type: 'server', file: '.env.local', description: 'OpenAI API key' },
16 { name: 'NEXT_PUBLIC_APP_NAME', value: 'QuantumCity', type: 'public', file: '.env', description: 'App name (visible in browser)' },
17 { name: 'NEXT_PUBLIC_API_URL', value: 'http://localhost:4000/api', type: 'public', file: '.env', description: 'API URL (visible in browser)' },
18 { name: 'NODE_ENV', value: 'development', type: 'server', file: 'system', description: 'Runtime environment' },
19];
20
21type EnvFile = '.env' | '.env.local' | '.env.production' | '.env.development';
22
23const fileHierarchy: { file: EnvFile; priority: number; description: string; gitIgnored: boolean }[] = [
24 { file: '.env.local', priority: 1, description: 'Surcharges locales (priorité la plus haute)', gitIgnored: true },
25 { file: '.env.development', priority: 2, description: 'Uniquement pour npm run dev', gitIgnored: false },
26 { file: '.env.production', priority: 2, description: 'Uniquement pour npm run build/start', gitIgnored: false },
27 { file: '.env', priority: 3, description: 'Valeurs par défaut', gitIgnored: false },
28];
29
30export default function EnvVarsDemo() {
31 const [activeTab, setActiveTab] = useState<'vars' | 'hierarchy' | 'validation'>('vars');
32 const [filter, setFilter] = useState<'all' | 'server' | 'public'>('all');
33 const [showValues, setShowValues] = useState(false);
34
35 const filtered = filter === 'all' ? envVars : envVars.filter(v => v.type === filter);
36
37 const zodSchema = `import { z } from 'zod';
38
39const serverEnvSchema = z.object({
40 DATABASE_URL: z.string().url(),
41 JWT_SECRET: z.string().min(32),
42 OPENAI_API_KEY: z.string().startsWith('sk-'),
43 NODE_ENV: z.enum(['development', 'production', 'test']),
44});
45
46const clientEnvSchema = z.object({
47 NEXT_PUBLIC_APP_NAME: z.string().min(1),
48 NEXT_PUBLIC_API_URL: z.string().url(),
49});
50
51// Validation au démarrage de l'application
52const result = serverEnvSchema.safeParse(process.env);
53if (!result.success) {
54 throw new Error('Variables manquantes !');
55}`;
56
57 return (
58 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
59 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Variables d'environnement</h1>
60 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Configuration de production de Next.js</p>
61
62 <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
63 {[
64 { id: 'vars' as const, label: 'Variables' },
65 { id: 'hierarchy' as const, label: 'Hiérarchie .env' },
66 { id: 'validation' as const, label: 'Validation Zod' },
67 ].map(t => (
68 <button key={t.id} onClick={() => setActiveTab(t.id)} style={{
69 padding: '8px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
70 background: activeTab === t.id ? '#64ffda' : '#1a1a2e', color: activeTab === t.id ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
71 }}>{t.label}</button>
72 ))}
73 </div>
74
75 {activeTab === 'vars' && (
76 <div>
77 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
78 {(['all', 'server', 'public'] as const).map(f => (
79 <button key={f} onClick={() => setFilter(f)} style={{
80 padding: '6px 12px', borderRadius: '6px', border: 'none', cursor: 'pointer', fontSize: '13px',
81 background: filter === f ? '#7c3aed' : '#1a1a2e', color: filter === f ? '#fff' : '#8892b0',
82 }}>{f === 'all' ? 'Toutes' : f === 'server' ? 'Serveur' : 'NEXT_PUBLIC_'}</button>
83 ))}
84 <button onClick={() => setShowValues(!showValues)} style={{
85 padding: '6px 12px', borderRadius: '6px', border: '1px solid #333', background: 'transparent', color: '#8892b0', cursor: 'pointer', fontSize: '13px', marginLeft: 'auto',
86 }}>{showValues ? 'Masquer les valeurs' : 'Afficher les valeurs'}</button>
87 </div>
88 <div style={{ display: 'grid', gap: '8px' }}>
89 {filtered.map(v => (
90 <div key={v.name} style={{
91 padding: '12px', borderRadius: '8px', background: '#1a1a2e',
92 border: '1px solid ' + (v.type === 'server' ? '#f4433640' : '#64ffda40'),
93 }}>
94 <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
95 <code style={{ color: v.type === 'server' ? '#f44336' : '#64ffda', fontWeight: 'bold', fontSize: '13px' }}>{v.name}</code>
96 <span style={{ fontSize: '10px', padding: '2px 6px', borderRadius: '4px', background: v.type === 'server' ? '#f4433620' : '#64ffda20', color: v.type === 'server' ? '#f44336' : '#64ffda' }}>
97 {v.type === 'server' ? 'ONLY SERVER' : 'PUBLIC (browser)'}
98 </span>
99 <span style={{ fontSize: '10px', color: '#666', marginLeft: 'auto' }}>{v.file}</span>
100 </div>
101 <p style={{ color: '#8892b0', fontSize: '12px', margin: '4px 0 0' }}>{v.description}</p>
102 {showValues && <code style={{ color: '#ff9800', fontSize: '12px', display: 'block', marginTop: '4px' }}>{v.value}</code>}
103 </div>
104 ))}
105 </div>
106 <div style={{ marginTop: '16px', padding: '12px', borderRadius: '8px', background: '#f4433610', border: '1px solid #f4433630' }}>
107 <p style={{ color: '#f44336', fontSize: '13px', margin: 0, fontWeight: 'bold' }}>Attention !</p>
108 <p style={{ color: '#8892b0', fontSize: '12px', margin: '4px 0 0' }}>Les variables SANS le préfixe NEXT_PUBLIC_ sont accessibles UNIQUEMENT sur le serveur. Elles n'arrivent jamais dans le navigateur !</p>
109 </div>
110 </div>
111 )}
112
113 {activeTab === 'hierarchy' && (
114 <div>
115 <h3 style={{ color: '#64ffda', fontSize: '16px', marginBottom: '12px' }}>Hiérarchie des fichiers .env (de la priorité la plus haute)</h3>
116 {fileHierarchy.map((f, i) => (
117 <div key={f.file} style={{
118 padding: '16px', marginBottom: '8px', borderRadius: '8px',
119 background: '#1a1a2e', border: '1px solid ' + (i === 0 ? '#64ffda40' : '#33333380'),
120 display: 'flex', alignItems: 'center', gap: '16px',
121 }}>
122 <span style={{ color: '#64ffda', fontSize: '24px', fontWeight: 'bold', width: '30px' }}>{f.priority}</span>
123 <div style={{ flex: 1 }}>
124 <code style={{ color: '#fff', fontWeight: 'bold' }}>{f.file}</code>
125 <p style={{ color: '#8892b0', fontSize: '12px', margin: '4px 0 0' }}>{f.description}</p>
126 </div>
127 <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '4px', background: f.gitIgnored ? '#f4433620' : '#4caf5020', color: f.gitIgnored ? '#f44336' : '#4caf50' }}>
128 {f.gitIgnored ? '.gitignore' : 'commité'}
129 </span>
130 </div>
131 ))}
132 </div>
133 )}
134
135 {activeTab === 'validation' && (
136 <div>
137 <h3 style={{ color: '#64ffda', fontSize: '16px', marginBottom: '12px' }}>Validation avec Zod (pattern t3-env)</h3>
138 <pre style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', fontSize: '12px', overflow: 'auto', border: '1px solid #333', whiteSpace: 'pre-wrap' }}>
139 {zodSchema}
140 </pre>
141 <p style={{ color: '#8892b0', fontSize: '13px', marginTop: '12px' }}>Valider les variables au démarrage de l'application évite les erreurs au runtime.</p>
142 </div>
143 )}
144 </div>
145 );
146}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que signifie le préfixe NEXT_PUBLIC_ pour les variables d'environnement dans Next.js ?
2. Quel fichier .env a la priorité la PLUS ÉLEVÉE dans Next.js ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.