Kurs Next.js · Moduł 8: Deployment i produkcja

Zmienne środowiskowe i konfiguracja produkcyjna

6 min czytania
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 środowisk

Każ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=production

Prefix 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ędzie

Przykł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:

  1. Pliki .env - hierarchia: .env.local > .env.production > .env
  2. NEXT_PUBLIC_ prefix - tylko te zmienne trafiają do przeglądarki
  3. Runtime vs Build-time - zmienne serwerowe są dynamiczne, klienckie zamrożone po build
  4. Walidacja z Zod - gwarantuje poprawność konfiguracji
  5. 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. 1. Co oznacza prefix NEXT_PUBLIC_ przy zmiennych środowiskowych w Next.js?

  2. 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.

Przydatne artykuły