21. Переменные окружения
22. Переменные окружения в Next.js 🔐
Заголовок раздела «22. Переменные окружения в Next.js 🔐»Представь: ты написал сервис и хочешь подключить базу данных. Пишешь прямо в коде: password: "super_secret_123". Коммитишь на GitHub. А там — публичный репозиторий… 💀
Именно для этого и существуют переменные окружения — секретные записки, которые хранятся вне кода!
Аналогия: Переменные окружения — как сейф в офисе. Ключи от сервера, пароли, API-токены лежат в сейфе (.env файл), а не на рабочем столе всех сотрудников (в коде). Каждый сотрудник знает, что “ключ от сейфа лежит в сейфе”, но сам ключ — секрет.
Что НЕЛЬЗЯ хардкодить в коде │ Что использовать вместо этого──────────────────────────────────┼──────────────────────────────────────────"password": "qwerty123" │ process.env.DB_PASSWORDapiKey: "sk-abc123def456" │ process.env.OPENAI_API_KEYhost: "prod.db.example.com" │ process.env.DATABASE_URLsecret: "jwt-super-secret" │ process.env.JWT_SECRET"https://api.staging.com" │ process.env.NEXT_PUBLIC_API_URL📁 Файлы .env и их приоритет
Заголовок раздела «📁 Файлы .env и их приоритет»Next.js поддерживает несколько .env файлов с разным приоритетом:
Приоритет загрузки (от высшего к низшему):──────────────────────────────────────────────1. .env.local ← ВСЕГДА загружается (кроме test), gitignore!2. .env.[mode].local ← .env.development.local или .env.production.local3. .env.[mode] ← .env.development или .env.production4. .env ← базовый файл, для всех окружений
Пример: .env → DATABASE_URL=postgres://localhost/mydb .env.local → DATABASE_URL=postgres://localhost/mydb_local
Итого: DATABASE_URL = postgres://localhost/mydb_local (локальный приоритет!)NEXT_PUBLIC_APP_NAME="My Awesome App"NEXT_PUBLIC_APP_VERSION="1.0.0"
# Значения по умолчанию (не секреты!)DEFAULT_LOCALE="en"MAX_FILE_SIZE_MB="10"# .env.local — СЕКРЕТЫ (в .gitignore! никогда не коммитить!)DATABASE_URL="postgresql://user:password@localhost:5432/mydb"JWT_SECRET="your-super-secret-jwt-key-here"OPENAI_API_KEY="sk-proj-abc123..."STRIPE_SECRET_KEY="sk_test_abc123..."NEXTAUTH_SECRET="random-secret-32-chars-minimum"NEXTAUTH_URL="http://localhost:3000"# .env.development — только для разработкиNEXT_PUBLIC_API_URL="http://localhost:3000/api"NEXT_PUBLIC_DEBUG_MODE="true"DATABASE_URL="postgresql://user:pass@localhost:5432/mydb_dev"LOG_LEVEL="debug"# .env.production — только для продакшенаNEXT_PUBLIC_API_URL="https://api.example.com"NEXT_PUBLIC_DEBUG_MODE="false"LOG_LEVEL="error"# DATABASE_URL берём из .env.local на сервере или из Vercel env vars# .env.test — для тестов (Jest, Playwright)DATABASE_URL="postgresql://user:pass@localhost:5432/mydb_test"NEXTAUTH_URL="http://localhost:3000"⚠️ Важно:
.env.localзагружается ВСЕГДА (кроме тестов, где используется.env.test). Добавь его в.gitignoreнемедленно!
🌐 NEXT_PUBLIC_ — что попадает в браузер?
Заголовок раздела «🌐 NEXT_PUBLIC_ — что попадает в браузер?»Это КРИТИЧЕСКИ важное различие! 🚨
// Серверные переменные (БЕЗОПАСНЫЕ) — только на сервереconst dbUrl = process.env.DATABASE_URL; // ✅ Только серверconst jwtSecret = process.env.JWT_SECRET; // ✅ Только серверconst apiKey = process.env.OPENAI_API_KEY; // ✅ Только сервер
// Публичные переменные (NEXT_PUBLIC_) — попадают в браузер!const appUrl = process.env.NEXT_PUBLIC_API_URL; // ⚠️ Видно в браузереconst appName = process.env.NEXT_PUBLIC_APP_NAME; // ⚠️ Видно в браузереКак это работает под капотом:
// Next.js при сборке делает буквальную замену строк!// Твой код:console.log(process.env.NEXT_PUBLIC_APP_NAME);
// Превращается в бандле в:console.log("My Awesome App"); // ← значение зашито в JS файл!
// Поэтому открыв DevTools → Sources → main.js, ты увидишь это значение!Что МОЖНО делать с NEXT_PUBLIC_ │ Что НЕЛЬЗЯ делать с NEXT_PUBLIC_───────────────────────────────────┼──────────────────────────────────────────URL публичного API │ Секретные ключи APIНазвание приложения │ Пароли базы данныхВерсия приложения │ JWT секретыID аналитики (GA4) │ Stripe/Paypal секретные ключиFeature flags (публичные) │ Ключи шифрованияПубличный ключ Stripe (pk_*) │ OAuth client secrets// ✅ Правильно — публичный ключ Stripe (pk_ — Public Key)const stripe = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
// ❌ ОПАСНО! Секретный ключ никогда не должен быть NEXT_PUBLIC_!// process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY ← НИКОГДА!💻 Использование в коде
Заголовок раздела «💻 Использование в коде»// app/page.tsx — Server Componentexport default async function HomePage() { // Серверные переменные доступны напрямую const apiUrl = process.env.INTERNAL_API_URL; // undefined в браузере const data = await fetch(`${apiUrl}/products`);
return <div>...</div>;}// components/Analytics.tsx — Client Component'use client';
import { useEffect } from 'react';
export function Analytics() { useEffect(() => { // Только NEXT_PUBLIC_ доступны в клиентских компонентах const gaId = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID; if (gaId) { // Инициализация Google Analytics window.gtag('config', gaId); } }, []);
return null;}// app/api/chat/route.ts — API Route (только сервер)import OpenAI from 'openai';import { NextRequest, NextResponse } from 'next/server';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, // Безопасно — только сервер!});
export async function POST(req: NextRequest) { const { message } = await req.json();
const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: message }], });
return NextResponse.json({ reply: response.choices[0].message.content });}🛡️ Валидация с Zod — делаем это правильно
Заголовок раздела «🛡️ Валидация с Zod — делаем это правильно»Просто использовать process.env.SOMETHING опасно. Что если переменная не задана? Приложение сломается в рантайме с непонятной ошибкой.
Решение: валидируем все переменные при старте приложения!
npm install zod// env.ts — центральный файл валидацииimport { z } from 'zod';
// Схема для серверных переменныхconst serverSchema = z.object({ // База данных DATABASE_URL: z.string().url('DATABASE_URL должен быть валидным URL'),
// Аутентификация NEXTAUTH_SECRET: z.string().min(32, 'NEXTAUTH_SECRET минимум 32 символа'), NEXTAUTH_URL: z.string().url(),
// Внешние сервисы OPENAI_API_KEY: z.string().startsWith('sk-', 'Некорректный OpenAI API key'), STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
// Опциональные LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), MAX_FILE_SIZE_MB: z.coerce.number().positive().default(10),
// Окружение NODE_ENV: z.enum(['development', 'production', 'test']),});
// Схема для публичных переменных (клиент + сервер)const clientSchema = z.object({ NEXT_PUBLIC_APP_NAME: z.string().min(1), NEXT_PUBLIC_API_URL: z.string().url(), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'), NEXT_PUBLIC_GA_MEASUREMENT_ID: z.string().optional(),});
// Валидируем при импорте модуляfunction validateEnv() { const serverParsed = serverSchema.safeParse(process.env);
if (!serverParsed.success) { console.error('❌ Ошибка валидации серверных env переменных:'); console.error(serverParsed.error.flatten().fieldErrors); throw new Error('Невалидные переменные окружения'); }
const clientParsed = clientSchema.safeParse(process.env);
if (!clientParsed.success) { console.error('❌ Ошибка валидации публичных env переменных:'); console.error(clientParsed.error.flatten().fieldErrors); throw new Error('Невалидные публичные переменные окружения'); }
return { server: serverParsed.data, client: clientParsed.data, };}
export const env = validateEnv();// Теперь env.server.DATABASE_URL — строго типизирован и гарантированно существует!// env.client.NEXT_PUBLIC_APP_NAME — тоже!// Использование в кодеimport { env } from '@/env';
// ✅ Строго типизировано, автодополнение работает!const dbUrl = env.server.DATABASE_URL; // stringconst appName = env.client.NEXT_PUBLIC_APP_NAME; // stringconst logLevel = env.server.LOG_LEVEL; // "debug" | "info" | "warn" | "error"🏆 t3-env — лучшая практика от T3 Stack
Заголовок раздела «🏆 t3-env — лучшая практика от T3 Stack»T3 Stack — популярный стек от Theo. Они создали библиотеку специально для типобезопасных env переменных в Next.js:
npm install @t3-oss/env-nextjs zod// env.ts — с @t3-oss/env-nextjsimport { createEnv } from '@t3-oss/env-nextjs';import { z } from 'zod';
export const env = createEnv({ /** * Серверные переменные — только Node.js * НЕ попадают в браузер */ server: { DATABASE_URL: z.string().url(), NEXTAUTH_SECRET: z.string().min(32), NEXTAUTH_URL: z.preprocess( (str) => process.env.VERCEL_URL ?? str, process.env.VERCEL ? z.string() : z.string().url() ), OPENAI_API_KEY: z.string().startsWith('sk-'), STRIPE_SECRET_KEY: z.string().startsWith('sk_'), NODE_ENV: z.enum(['development', 'production', 'test']), },
/** * Клиентские переменные — попадают в браузер * ОБЯЗАТЕЛЬНО должны начинаться с NEXT_PUBLIC_ */ client: { NEXT_PUBLIC_APP_URL: z.string().url(), NEXT_PUBLIC_APP_NAME: z.string().min(1), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'), },
/** * Маппинг переменных (нужен для проверки) * Убеждаемся что переменные действительно берутся из process.env */ runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, NEXTAUTH_URL: process.env.NEXTAUTH_URL, OPENAI_API_KEY: process.env.OPENAI_API_KEY, STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, },
/** * По умолчанию @t3-oss/env-nextjs выдаёт ошибку если * серверные переменные используются в клиентском коде. * skipValidation=true для тестов или CI без полных env */ skipValidation: !!process.env.SKIP_ENV_VALIDATION,});// Использование — максимально удобно!import { env } from '@/env';
// Серверconst client = new OpenAI({ apiKey: env.OPENAI_API_KEY });await db.connect(env.DATABASE_URL);
// Клиентconst stripe = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);document.title = env.NEXT_PUBLIC_APP_NAME;🔒 Secrets Management
Заголовок раздела «🔒 Secrets Management»Vercel Environment Variables
Заголовок раздела «Vercel Environment Variables»# Через Vercel CLIvercel env add DATABASE_URL productionvercel env add JWT_SECRET production
# Просмотрvercel env ls
# Удалениеvercel env rm DATABASE_URL productionВ Vercel Dashboard: Settings → Environment Variables. Можно задавать разные значения для Preview/Development/Production.
Vercel окружения:──────────────────────────────────────────Production ← Только main веткаPreview ← Pull Request веткиDevelopment ← vercel dev (локально)GitHub Actions Secrets
Заголовок раздела «GitHub Actions Secrets»name: Deploy
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest env: # Берём секреты из GitHub Secrets DATABASE_URL: ${{ secrets.DATABASE_URL }} NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} steps: - uses: actions/checkout@v4 - name: Install dependencies run: npm ci - name: Build run: npm run build env: NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}# Через GitHub CLIgh secret set DATABASE_URLgh secret set NEXTAUTH_SECRET --body "$(openssl rand -base64 32)"gh secret list⏱️ Runtime vs Build-time переменные
Заголовок раздела «⏱️ Runtime vs Build-time переменные»Это тонкий, но важный момент! 🎯
Build-time (при сборке) │ Runtime (при запуске)─────────────────────────────────────┼─────────────────────────────────────────NEXT_PUBLIC_* переменные │ Серверные переменные (без NEXT_PUBLIC_)Зашиваются в JS бандл │ Читаются из process.env при запросеМеняются ТОЛЬКО при пересборке │ Можно менять без пересборкиОдинаковы для всех пользователей │ Могут быть разными на разных серверах// ⚠️ Ловушка! NEXT_PUBLIC_ — build-time!// Если ты изменишь NEXT_PUBLIC_API_URL на сервере без пересборки — ничего не изменится!// Значение уже зашито в бандл.
// Решение: для динамических публичных значений используй API endpoint// app/api/config/route.tsexport async function GET() { return Response.json({ // Эти значения читаются в РАНТАЙМЕ каждый раз! featureFlags: { newCheckout: process.env.FEATURE_NEW_CHECKOUT === 'true', darkMode: process.env.FEATURE_DARK_MODE === 'true', }, });}// next.config.mjs — можно задавать env прямо в конфиге/** @type {import('next').NextConfig} */const nextConfig = { env: { // Это значение будет доступно как process.env.BUILD_TIME // и зашито в бандл при сборке BUILD_TIME: new Date().toISOString(), APP_VERSION: process.env.npm_package_version, },};
export default nextConfig;🐳 Docker и переменные окружения
Заголовок раздела «🐳 Docker и переменные окружения»# Dockerfile — правильный подход
FROM node:20-alpine AS base
# --- Зависимости ---FROM base AS depsWORKDIR /appCOPY package*.json ./RUN npm ci
# --- Сборка ---FROM base AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .
# NEXT_PUBLIC_ нужны при сборке!# Передаём через build argsARG NEXT_PUBLIC_API_URLARG NEXT_PUBLIC_APP_NAMEENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URLENV NEXT_PUBLIC_APP_NAME=$NEXT_PUBLIC_APP_NAME
# Серверные секреты НЕ должны быть в образе!# Они будут переданы при запуске контейнераRUN npm run build
# --- Production ---FROM base AS runnerWORKDIR /appENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./COPY --from=builder /app/.next/static ./.next/staticCOPY --from=builder /app/public ./public
EXPOSE 3000CMD ["node", "server.js"]version: '3.8'services: app: build: context: . args: # Build-time переменные (NEXT_PUBLIC_) NEXT_PUBLIC_API_URL: https://api.example.com NEXT_PUBLIC_APP_NAME: My App ports: - "3000:3000" environment: # Runtime переменные (серверные секреты) # НЕ зашиваются в образ, передаются при запуске DATABASE_URL: ${DATABASE_URL} JWT_SECRET: ${JWT_SECRET} NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} env_file: - .env.production.local # Или через secrets в swarm/k8s# Запуск с переменнымиdocker run \ -e DATABASE_URL="postgresql://..." \ -e JWT_SECRET="secret" \ -p 3000:3000 \ my-next-app
# Или через файлdocker run --env-file .env.production -p 3000:3000 my-next-app😱 Типичные ошибки и как их избежать
Заголовок раздела «😱 Типичные ошибки и как их избежать»Ошибка 1: Случайно выставить секрет через NEXT_PUBLIC_
Заголовок раздела «Ошибка 1: Случайно выставить секрет через NEXT_PUBLIC_»# ❌ КАТАСТРОФА! Stripe секретный ключ в браузере!NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc123...
# ✅ Правильно — секретный ключ только на сервереSTRIPE_SECRET_KEY=sk_live_abc123...# А публичный ключ — через NEXT_PUBLIC_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xyz789...Ошибка 2: Undefined в браузере
Заголовок раздела «Ошибка 2: Undefined в браузере»// ❌ Не работает в Client Components!'use client';const dbUrl = process.env.DATABASE_URL; // undefined в браузере!
// ✅ Только NEXT_PUBLIC_ в клиентском кодеconst apiUrl = process.env.NEXT_PUBLIC_API_URL; // работает!
// ✅ Или передавай через props из Server Component// Server Component:export default function Layout() { return <ClientComp config={{ apiUrl: process.env.INTERNAL_API_URL }} />;}Ошибка 3: Нет .env.example
Заголовок раздела «Ошибка 3: Нет .env.example»# ❌ Новый разработчик клонирует проект и не знает какие переменные нужны
# ✅ Создавай .env.example (коммитить в git! без реальных значений)# .env.example — документация для команды (коммитить!)DATABASE_URL="postgresql://user:password@localhost:5432/mydb"NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32"NEXTAUTH_URL="http://localhost:3000"OPENAI_API_KEY="sk-..."STRIPE_SECRET_KEY="sk_test_..."NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."NEXT_PUBLIC_APP_URL="http://localhost:3000"NEXT_PUBLIC_APP_NAME="My App"Ошибка 4: Нет .gitignore для .env файлов
Заголовок раздела «Ошибка 4: Нет .gitignore для .env файлов»# .gitignore — убедись что это есть!.env.env.local.env*.local# НО: .env.example и .env.development (без секретов) — можно коммититьОшибка 5: Опечатка в названии переменной
Заголовок раздела «Ошибка 5: Опечатка в названии переменной»// ❌ Опечатка — получим undefined молчаconst key = process.env.OPNEAI_API_KEY; // Typo! OPNEAI вместо OPENAI
// ✅ С zod валидацией сразу увидишь ошибку при старте!// ❌ Ошибка: "OPENAI_API_KEY" is required but received undefined📄 .env.example как документация команды
Заголовок раздела «📄 .env.example как документация команды»Отличная практика — держать .env.example актуальным с комментариями:
# .env.example — копируй в .env.local и заполняй реальными значениями# Генерация: cp .env.example .env.local
# ═══════════════════════════════════════# БАЗА ДАННЫХ# ═══════════════════════════════════════# PostgreSQL URL. Для локальной разработки: docker-compose up -d dbDATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_dev"
# ═══════════════════════════════════════# АУТЕНТИФИКАЦИЯ (NextAuth.js)# ═══════════════════════════════════════# Генерация: openssl rand -base64 32NEXTAUTH_SECRET=""# URL приложения. В продакшене: https://your-domain.comNEXTAUTH_URL="http://localhost:3000"
# GitHub OAuth (создай на github.com/settings/apps)GITHUB_CLIENT_ID=""GITHUB_CLIENT_SECRET=""
# ═══════════════════════════════════════# ПЛАТЕЖИ (Stripe)# ═══════════════════════════════════════# Тестовые ключи: dashboard.stripe.com/test/apikeysSTRIPE_SECRET_KEY="sk_test_..."NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."STRIPE_WEBHOOK_SECRET="whsec_..."
# ═══════════════════════════════════════# AI (OpenAI)# ═══════════════════════════════════════# Получи на: platform.openai.com/api-keysOPENAI_API_KEY="sk-proj-..."
# ═══════════════════════════════════════# ПУБЛИЧНЫЕ ПЕРЕМЕННЫЕ (видны в браузере!)# ═══════════════════════════════════════NEXT_PUBLIC_APP_NAME="My Awesome App"NEXT_PUBLIC_APP_URL="http://localhost:3000"NEXT_PUBLIC_GA_MEASUREMENT_ID="G-XXXXXXXXXX"⚙️ next.config.mjs — загрузка и переопределение
Заголовок раздела «⚙️ next.config.mjs — загрузка и переопределение»import { fileURLToPath } from 'node:url';import path from 'node:path';
/** @type {import('next').NextConfig} */const nextConfig = { // Добавить переменные окружения в бандл env: { CUSTOM_KEY: 'my-value', BUILD_TIMESTAMP: new Date().toISOString(), },
// Rewrites используют env переменные async rewrites() { return [ { source: '/api/proxy/:path*', destination: `${process.env.BACKEND_URL}/:path*`, }, ]; },
// Проверка переменных при сборке webpack(config) { const required = ['DATABASE_URL', 'NEXTAUTH_SECRET']; const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0 && process.env.NODE_ENV === 'production') { throw new Error( `Отсутствуют обязательные env переменные: ${missing.join(', ')}` ); }
return config; },};
export default nextConfig;🔐 Генерация секретов
Заголовок раздела «🔐 Генерация секретов»# Генерация NEXTAUTH_SECRET (32 байта = 64 hex символа)openssl rand -base64 32
# Или через Node.jsnode -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# JWT Secretnode -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# Для быстрой настройки используй Vercel CLIvercel env pull .env.local# Скачивает переменные из Vercel проекта в .env.local📊 Итоговая шпаргалка
Заголовок раздела «📊 Итоговая шпаргалка»Файл │ Gitignore? │ Когда загружается────────────────────┼────────────┼──────────────────────────────────────.env │ Нет │ Всегда.env.local │ Да! │ Всегда (кроме test).env.development │ Нет │ next dev.env.production │ Нет │ next build + next start.env.test │ Нет │ jest, playwright.env.*.local │ Да! │ Соответствующий режим.env.example │ Нет │ Никогда (только документация)Переменная │ Server │ Client │ Пример использования────────────────────────┼────────┼────────┼──────────────────────────────DATABASE_URL │ ✅ │ ❌ │ Подключение к БДJWT_SECRET │ ✅ │ ❌ │ Подпись токеновOPENAI_API_KEY │ ✅ │ ❌ │ OpenAI запросыNEXT_PUBLIC_APP_NAME │ ✅ │ ✅ │ Название в заголовкеNEXT_PUBLIC_GA_ID │ ✅ │ ✅ │ Google AnalyticsNEXT_PUBLIC_API_URL │ ✅ │ ✅ │ URL публичного API🎯 Чек-лист переменных окружения
Заголовок раздела «🎯 Чек-лист переменных окружения»✅ .env.example создан и актуален (с комментариями)✅ .env.local в .gitignore✅ Все секреты без NEXT_PUBLIC_ префикса✅ Валидация через zod или @t3-oss/env-nextjs✅ Переменные добавлены в Vercel/CI/CD✅ .env.test для тестовых значений✅ Docker build args для NEXT_PUBLIC_ переменных✅ Нет секретов в git истории (проверь!)✅ NEXTAUTH_SECRET сгенерирован безопасно✅ Нет захардкоженных URL, ключей, паролей в кодеТеперь твои секреты — под надёжной охраной! 🔐🛡️