Kurs Next.js · Moduł 8: Deployment i produkcja
Zmienne środowiskowe i konfiguracja produkcyjna
W tej lekcji8
W Metropolii Quantum 2150 każdy system wymaga precyzyjnej konfiguracji środowiskowej. Tak jak w cyberpunkowym mieście każda dzielnica ma własne protokoły bezpieczeństwa, tak aplikacja Next.js potrzebuje różnych ustawień dla różnych środowisk - development, staging i production. Zmienne środowiskowe to klucz do bezpiecznego i elastycznego zarządzania konfiguracją.
Czym są zmienne środowiskowe?
Zmienne środowiskowe (environment variables) to pary klucz-wartość, które konfigurują zachowanie aplikacji bez modyfikacji kodu źródłowego. Dzięki nim możesz przechowywać:
- Klucze API i sekrety (np. klucze do bazy danych, tokeny autoryzacji)
- Adresy URL serwisów zewnętrznych
- Flagi konfiguracyjne (np. tryb debug, poziom logowania)
- Ustawienia specyficzne dla środowiska (development vs production)
Pliki .env w Next.js
Next.js ma wbudowane wsparcie dla plików .env. Framework automatycznie ładuje zmienne środowiskowe z kilku plików, w określonej kolejności priorytetów:
Hierarchia plików .env
1# Pliki .env w Next.js (od najwyższego priorytetu):
2.env.local # Lokalne nadpisania - NIGDY nie commituj do git!
3.env.development # Tylko dla npm run dev
4.env.production # Tylko dla npm run build / npm start
5.env # Domyślne wartości dla wszystkich środowiskKażdy plik może nadpisywać wartości z plików o niższym priorytecie. Plik .env.local ma zawsze najwyższy priorytet i powinien być w .gitignore.
Tworzenie plików .env
1# .env - domyślne wartości (commitowane do 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 - lokalne sekrety (NIE commituj!)
8DATABASE_URL=mongodb://user:secret@prod-cluster.mongodb.net/quantum
9JWT_SECRET=super-tajny-klucz-produkcyjny-2150
10OPENAI_API_KEY=sk-proj-abc123...
11STRIPE_SECRET_KEY=sk_live_abc123...
12
13# .env.production - ustawienia produkcyjne
14NEXT_PUBLIC_API_URL=https://api.quantum-city.com
15NEXT_PUBLIC_APP_NAME=QuantumCity Production
16NODE_ENV=productionPrefix NEXT_PUBLIC_ - klucz do bezpieczeństwa
To jedna z najważniejszych zasad w Next.js! Prefix NEXT_PUBLIC_ determinuje, czy zmienna jest dostępna w przeglądarce:
1// Zmienne BEZ prefixu - dostępne TYLKO na serwerze
2// Nigdy nie trafiają do bundla przeglądarki!
3const dbUrl = process.env.DATABASE_URL; // OK w Server Components
4const apiKey = process.env.OPENAI_API_KEY; // OK w Route Handlers
5const secret = process.env.JWT_SECRET; // OK w middleware
6
7// Zmienne Z prefixem NEXT_PUBLIC_ - dostępne WSZĘDZIE
8// Trafiają do bundla JavaScript w przeglądarce!
9const appName = process.env.NEXT_PUBLIC_APP_NAME; // OK w Client Components
10const apiUrl = process.env.NEXT_PUBLIC_API_URL; // OK wszędziePrzykład użycia w komponentach
1// app/layout.tsx - Server Component
2export default function RootLayout({ children }: { children: React.ReactNode }) {
3 // Bezpieczne - zmienna serwerowa, nie trafia do przeglądarki
4 const dbUrl = process.env.DATABASE_URL;
5 console.log('Connecting to:', dbUrl);
6
7 return (
8 <html>
9 <body>
10 {/* NEXT_PUBLIC_ - bezpiecznie używane w 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 // NEXT_PUBLIC_ zmienne działają w przeglądarce
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}Runtime vs Build-time zmienne środowiskowe
Ważne jest zrozumienie, kiedy zmienne są odczytywane:
Build-time (czas budowania)
Zmienne NEXT_PUBLIC_ są wstrzykiwane podczas npm run build. Oznacza to, że po zbudowaniu aplikacji ich wartości są "zamrożone" w kodzie JavaScript:
1# Te wartości zostaną wstrzyknięte do bundla podczas build:
2NEXT_PUBLIC_API_URL=https://api.quantum-city.com npm run build
3
4# Po zbudowaniu, zmiana tej zmiennej NIE zmieni zachowania aplikacji!
5# Trzeba przebudować (rebuild), aby zastosować nową wartość.Runtime (czas wykonywania)
Zmienne serwerowe (bez NEXT_PUBLIC_) są odczytywane w czasie rzeczywistym - przy każdym żądaniu:
1// app/api/config/route.ts
2import { NextResponse } from 'next/server';
3
4export async function GET() {
5 // Ta wartość jest odczytywana PRZY KAŻDYM żądaniu
6 // Zmiana zmiennej na serwerze natychmiast wpływa na odpowiedź
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}Walidacja zmiennych środowiskowych z Zod (wzorzec t3-env)
W profesjonalnych aplikacjach warto walidować zmienne środowiskowe przy starcie aplikacji, aby uniknąć błędów w runtime. Popularne podejście to użycie biblioteki Zod:
1// lib/env.ts
2import { z } from 'zod';
3
4// Schemat walidacji zmiennych serwerowych
5const serverEnvSchema = z.object({
6 DATABASE_URL: z.string().url('DATABASE_URL musi być poprawnym URL'),
7 JWT_SECRET: z.string().min(32, 'JWT_SECRET musi mieć min. 32 znaki'),
8 OPENAI_API_KEY: z.string().startsWith('sk-', 'Nieprawidłowy klucz OpenAI'),
9 NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
10 PORT: z.coerce.number().default(4000),
11});
12
13// Schemat walidacji zmiennych klienckich
14const clientEnvSchema = z.object({
15 NEXT_PUBLIC_APP_NAME: z.string().min(1),
16 NEXT_PUBLIC_API_URL: z.string().url(),
17});
18
19// Walidacja przy imporcie modułu
20const serverEnv = serverEnvSchema.safeParse(process.env);
21const clientEnv = clientEnvSchema.safeParse(process.env);
22
23if (!serverEnv.success) {
24 console.error('Brakujące zmienne serwerowe:', serverEnv.error.format());
25 throw new Error('Nieprawidłowa konfiguracja środowiska serwerowego');
26}
27
28if (!clientEnv.success) {
29 console.error('Brakujące zmienne klienckie:', clientEnv.error.format());
30 throw new Error('Nieprawidłowa konfiguracja środowiska klienckiego');
31}
32
33export const env = {
34 ...serverEnv.data,
35 ...clientEnv.data,
36};Użycie walidowanych zmiennych
1// app/api/users/route.ts
2import { env } from '@/lib/env';
3
4export async function GET() {
5 // TypeScript zna typy - env.DATABASE_URL jest zawsze string
6 // Walidacja gwarantuje, że wartość jest poprawnym URL
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}Konfiguracja next.config.js pod różne środowiska
Plik next.config.js może wykorzystywać zmienne środowiskowe do dynamicznej konfiguracji:
1// next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4 // Ustawianie dodatkowych zmiennych środowiskowych
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 // Konfiguracja obrazów zależna od środowiska
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 // Różne nagłówki dla dev i 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;Zarządzanie sekretami w produkcji
W środowisku produkcyjnym nigdy nie przechowujemy sekretów w plikach .env. Zamiast tego używamy platform do zarządzania sekretami:
1# Vercel - dodawanie zmiennych przez CLI
2vercel env add DATABASE_URL production
3vercel env add JWT_SECRET production
4vercel env add OPENAI_API_KEY production
5
6# Vercel - podgląd zmiennych
7vercel env ls
8
9# Docker - zmienne przez 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 - sekrety w CI/CD
18# Settings → Secrets and variables → Actions
19# Użycie: ${{ secrets.DATABASE_URL }}Podsumowanie
Zmienne środowiskowe to fundament bezpiecznej konfiguracji aplikacji Next.js:
- Pliki .env - hierarchia:
.env.local>.env.production>.env - NEXT_PUBLIC_ prefix - tylko te zmienne trafiają do przeglądarki
- Runtime vs Build-time - zmienne serwerowe są dynamiczne, klienckie zamrożone po build
- Walidacja z Zod - gwarantuje poprawność konfiguracji
- Sekrety w produkcji - nigdy w plikach, zawsze przez platformę hostingową
Przećwicz te koncepcje w edytorze poniżej.
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Demo: Zmienne środowiskowe i konfiguracja produkcyjna
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: 'URL bazy danych' },
14 { name: 'JWT_SECRET', value: 'dev-secret-key-change-me', type: 'server', file: '.env.local', description: 'Klucz JWT (tajny!)' },
15 { name: 'OPENAI_API_KEY', value: 'sk-proj-abc123...', type: 'server', file: '.env.local', description: 'Klucz API OpenAI' },
16 { name: 'NEXT_PUBLIC_APP_NAME', value: 'QuantumCity', type: 'public', file: '.env', description: 'Nazwa aplikacji (widoczna w przegladarce)' },
17 { name: 'NEXT_PUBLIC_API_URL', value: 'http://localhost:4000/api', type: 'public', file: '.env', description: 'URL API (widoczny w przegladarce)' },
18 { name: 'NODE_ENV', value: 'development', type: 'server', file: 'system', description: 'Srodowisko uruchomieniowe' },
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: 'Lokalne nadpisania (najwyzszy priorytet)', gitIgnored: true },
25 { file: '.env.development', priority: 2, description: 'Tylko dla npm run dev', gitIgnored: false },
26 { file: '.env.production', priority: 2, description: 'Tylko dla npm run build/start', gitIgnored: false },
27 { file: '.env', priority: 3, description: 'Domyslne wartosci', 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// Walidacja przy starcie aplikacji
52const result = serverEnvSchema.safeParse(process.env);
53if (!result.success) {
54 throw new Error('Brakujace zmienne!');
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' }}>Zmienne Srodowiskowe</h1>
60 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Konfiguracja produkcyjna Next.js</p>
61
62 <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
63 {[
64 { id: 'vars' as const, label: 'Zmienne' },
65 { id: 'hierarchy' as const, label: 'Hierarchia .env' },
66 { id: 'validation' as const, label: 'Walidacja 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' ? 'Wszystkie' : f === 'server' ? 'Serwerowe' : '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 ? 'Ukryj wartosci' : 'Pokaz wartosci'}</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' }}>Uwaga!</p>
108 <p style={{ color: '#8892b0', fontSize: '12px', margin: '4px 0 0' }}>Zmienne BEZ prefixu NEXT_PUBLIC_ sa dostepne TYLKO na serwerze. Nigdy nie trafia do przegladarki!</p>
109 </div>
110 </div>
111 )}
112
113 {activeTab === 'hierarchy' && (
114 <div>
115 <h3 style={{ color: '#64ffda', fontSize: '16px', marginBottom: '12px' }}>Hierarchia plikow .env (od najwyzszego priorytetu)</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' : 'commitowany'}
129 </span>
130 </div>
131 ))}
132 </div>
133 )}
134
135 {activeTab === 'validation' && (
136 <div>
137 <h3 style={{ color: '#64ffda', fontSize: '16px', marginBottom: '12px' }}>Walidacja z Zod (wzorzec 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' }}>Walidacja zmiennych przy starcie aplikacji zapobiega bledom w runtime.</p>
142 </div>
143 )}
144 </div>
145 );
146}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza prefix NEXT_PUBLIC_ przy zmiennych środowiskowych w Next.js?
2. Który plik .env ma NAJWYŻSZY priorytet w Next.js?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.