Урок 05

Переменные окружения на проде

Переменные на продакшене нужно хранить и передавать безопасно.

Где хранить

  • Vercel — Settings → Environment Variables
  • Docker — .env.production или через секреты
  • GitHub Actions — Secrets
  • AWS — Secrets Manager, SSM
  • HashiCorp Vault — для больших команд

Не в репозитории

Никогда не коммить .env с секретами. Только .env.example.

.env
.env.local
.env.production
.env*.local

На Vercel

Три окружения:

  • Production — для main
  • Preview — для PR
  • Development — для vercel dev

Разные значения для preview и prod — тестовые базы.

В Docker

Через --env-file

docker run --env-file .env.production my-app

Через environment в compose

services:
  app:
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - AUTH_SECRET=${AUTH_SECRET}

Значения — из .env рядом с docker-compose.yml или из переменных окружения хоста.

Docker Secrets

secrets:
  db_password:
    file: ./secrets/db_password.txt

services:
  app:
    secrets:
      - db_password

В контейнере доступно по /run/secrets/db_password.

Валидация переменных

Проверяй при старте:

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
    DATABASE_URL: z.string().url(),
    AUTH_SECRET: z.string().min(32),
    NEXT_PUBLIC_API_URL: z.string().url(),
    NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});

export const env = envSchema.parse(process.env);

Использование:

import { env } from '@/lib/env';

const db = new PrismaClient({ datasources: { db: { url: env.DATABASE_URL } } });

Приложение упадёт сразу, если переменная отсутствует — не в рантайме.

Клиентские переменные

Только NEXT_PUBLIC_*:

const apiUrl = process.env.NEXT_PUBLIC_API_URL;

Значение подставляется на этапе сборки. Если меняешь — нужен rebuild.

Runtime переменные

Серверные переменные читаются в рантайме:

const secret = process.env.AUTH_SECRET;

Можно менять без пересборки (для Docker).

NEXT_PUBLIC на проде

Проблема: NEXT_PUBLIC_* фиксируются при сборке. Если у тебя Docker-образ и нужно менять URL без пересборки — есть трюки:

Подстановка в рантайме

// app/layout.tsx
export default function RootLayout({ children }) {
    return (
        <html>
            <head>
                <script
                    dangerouslySetInnerHTML={{
                        __html: `window.__ENV__ = ${JSON.stringify({
                            API_URL: process.env.NEXT_PUBLIC_API_URL,
                        })}`,
                    }}
                />
            </head>
            <body>{children}</body>
        </html>
    );
}

Но обычно проще пересобрать образ.

Секреты

Не логируй:

console.log(process.env); // плохо
console.log(process.env.DATABASE_URL); // тоже плохо

Не возвращай клиенту:

return Response.json({ secret: process.env.AUTH_SECRET }); // опасно

Ротация

Меняй секреты периодически:

  1. Создай новый секрет
  2. Обнови переменную
  3. Перезапусти приложение
  4. Убедись, что всё работает
  5. Отзови старый

Итоги

  • Переменные не в репозитории
  • Разные окружения для preview и prod
  • Валидация через Zod при старте
  • NEXT_PUBLIC_* — на этапе сборки
  • Серверные — в рантайме
  • Не логируй секреты