Environment Variablen in React Builds sicher verwalten
AI generated
</>
{ }
React · Environment Variablen · Vite · Next.js
Environment Variablen in React Builds
sicher verwalten statt Secrets im Bundle

Environment Variablen in React Builds werden anders verarbeitet als in einem Backend: sie landen fest im Bundle, sobald sie das richtige Präfix tragen. Wer das nicht versteht, baut Secrets versehentlich in öffentlich einsehbaren JavaScript-Code ein oder wundert sich, warum eine geänderte Variable nach dem Deployment nicht greift.

17 Min. Lesezeit Vite · Next.js · Docker · Zod Build Time · Runtime Config

1. Build Time gegen Runtime: das grundlegende Missverständnis

Der häufigste Fehler im Umgang mit Environment Variablen in React entsteht, weil Entwicklerinnen und Entwickler aus dem Backend eine falsche Erwartung mitbringen. In Node.js oder PHP wird eine Environment Variable zur Laufzeit gelesen, ein Neustart des Prozesses genügt, um einen neuen Wert zu übernehmen. Environment Variablen in React funktionieren fundamental anders, weil der gesamte Code vor der Auslieferung zu statischen Dateien kompiliert wird, lange bevor irgendein Nutzer die Seite aufruft.

Diese Build-Time-Natur bedeutet, dass jede process.env- oder import.meta.env-Referenz vom Bundler durch den tatsächlichen Wert ersetzt wird, den die Variable zum Zeitpunkt des Builds hatte. Ändert sich die Variable danach auf dem Server, hat das keinerlei Effekt, solange kein neuer Build ausgeführt wird. Wer Environment Variablen in React wie im Backend behandelt und nur den Server neu startet, wird sich über ausbleibende Änderungen wundern und stundenlang an der falschen Stelle suchen.

Für Teams mit mehreren Umgebungen wie Development, Staging und Production heißt das konkret: für jede Umgebung braucht es einen eigenen Build, nicht nur eine eigene .env-Datei zur Laufzeit. Die folgenden Abschnitte zeigen, wie Vite und Next.js mit dieser Einschränkung umgehen und wie sich trotzdem eine flexible Konfiguration für Environment Variablen in React erreichen lässt.

2. Vite: import.meta.env und das VITE_ Präfix

Vite liest Environment Variablen in React-Projekten über import.meta.env aus, ersetzt aber aus Sicherheitsgründen nur Variablen mit dem Präfix VITE_. Diese bewusste Einschränkung verhindert, dass versehentlich jede Systemvariable, etwa ein Datenbank-Passwort aus der Shell-Umgebung, im öffentlichen Client-Bundle landet. Nur explizit mit VITE_ markierte Variablen gelten als für den Client bestimmt.

Die eingebauten Variablen import.meta.env.MODE, import.meta.env.DEV und import.meta.env.PROD stehen ohne eigenes Präfix zur Verfügung und erlauben umgebungsabhängige Logik direkt im Code, etwa das Deaktivieren von Analytics im Entwicklungsmodus.


// src/config.ts — reading Vite environment variables
interface AppConfig {
  apiBaseUrl: string;
  sentryDsn: string | undefined;
  isProduction: boolean;
}

export const config: AppConfig = {
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
  sentryDsn: import.meta.env.VITE_SENTRY_DSN,
  isProduction: import.meta.env.PROD,
};

// Fails fast if a required variable is missing at build time
if (!config.apiBaseUrl) {
  throw new Error("VITE_API_BASE_URL is not set — check your .env file");
}

Ein Detail, das häufig übersehen wird: import.meta.env Werte sind zur Build-Zeit immer Strings, auch wenn .env etwas wie VITE_MAX_RETRIES=3 enthält. Wer eine Zahl braucht, muss explizit mit Number() konvertieren. Für Environment Variablen in React-Projekten mit Vite gilt außerdem: nur Variablen, die tatsächlich im Code referenziert werden, landen im Bundle, tote Referenzen werden vom Bundler nicht mit eingebettet.

3. Next.js: NEXT_PUBLIC_ Präfix und Server gegen Client Env

Next.js unterscheidet bei Environment Variablen in React-Projekten strikt zwischen serverseitigen und clientseitigen Variablen. Alles ohne das Präfix NEXT_PUBLIC_ ist standardmäßig nur in Server Components, API-Routen und während des Builds selbst verfügbar, niemals im Browser. Erst das Präfix NEXT_PUBLIC_ macht eine Variable explizit für Client Components zugänglich und bettet sie beim Build in den JavaScript-Code ein.

Diese Trennung ist einer der größten Vorteile von Next.js gegenüber einem reinen Vite-Setup: Datenbank-Zugangsdaten oder API-Keys für Drittanbieter können ganz normal ohne Präfix definiert werden und bleiben serverseitig, solange sie ausschließlich in Server Components oder Route Handlers gelesen werden.


// app/api/orders/route.ts — server-only environment variable, never exposed
import { NextResponse } from "next/server";

// No NEXT_PUBLIC_ prefix — stays on the server, safe for secrets
const PAYMENT_API_KEY = process.env.PAYMENT_API_KEY;

export async function POST(request: Request) {
  if (!PAYMENT_API_KEY) {
    return NextResponse.json({ error: "Server misconfigured" }, { status: 500 });
  }

  const body = await request.json();
  const response = await fetch("https://payments.example.com/charge", {
    method: "POST",
    headers: { Authorization: `Bearer ${PAYMENT_API_KEY}` },
    body: JSON.stringify(body),
  });

  return NextResponse.json(await response.json());
}

// app/components/analytics-banner.tsx — client component, needs NEXT_PUBLIC_
"use client";
const analyticsId = process.env.NEXT_PUBLIC_ANALYTICS_ID;

Ein häufiger Fehler bei Environment Variablen in React-Projekten mit Next.js: destructuring von process.env, also const { API_KEY } = process.env. Next.js ersetzt Variablen nur bei direktem statischem Zugriff wie process.env.API_KEY, weil das Ersetzen zur Build-Zeit über statische Code-Analyse funktioniert. Destructuring oder dynamische Schlüssel wie process.env[key] werden nicht erkannt und liefern zur Laufzeit undefined.

4. .env-Dateien richtig strukturieren

Für Environment Variablen in React-Projekten hat sich eine Konvention aus mehreren .env-Dateien etabliert, die je nach Kontext geladen werden. .env enthält Standardwerte für alle Umgebungen, .env.local überschreibt diese lokal und wird nie eingecheckt, .env.production und .env.development gelten nur für den jeweiligen Modus. Vite und Next.js laden diese Dateien automatisch in der richtigen Reihenfolge, wobei spezifischere Dateien allgemeinere überschreiben.

Die Datei .env.example dokumentiert alle benötigten Variablen ohne echte Werte und gehört ins Repository, damit neue Teammitglieder sofort sehen, welche Environment Variablen in React-Setups sie selbst konfigurieren müssen. Ein sauberer .gitignore-Eintrag für .env.local und .env*.local verhindert, dass echte Zugangsdaten versehentlich ins Repository gelangen. Diese Struktur ist Standard bei Vite und Next.js gleichermaßen und lässt sich in beiden Frameworks ohne zusätzliche Bibliotheken nutzen.

5. Secrets niemals im Client-Bundle: Build-Time-Einbettung verstehen

Der kritischste Sicherheitsaspekt bei Environment Variablen in React ist, dass alles mit VITE_ oder NEXT_PUBLIC_ Präfix als String direkt im ausgelieferten JavaScript-Bundle landet und für jeden Besucher der Seite über die Browser-Entwicklertools einsehbar ist. Ein API-Key mit vollen Schreibrechten, versehentlich mit VITE_ präfixiert, ist damit faktisch öffentlich, unabhängig davon, ob er irgendwo in der Oberfläche angezeigt wird.

Die Prüfung, ob ein Secret versehentlich im Bundle gelandet ist, lässt sich einfach automatisieren: nach dem Build wird das dist-Verzeichnis nach bekannten Geheimnis-Mustern durchsucht.


#!/usr/bin/env bash
# check-bundle-secrets.sh — fail the build if secrets leaked into the bundle
set -euo pipefail

BUNDLE_DIR="dist/assets"
PATTERNS=("sk_live_" "AKIA" "-----BEGIN PRIVATE KEY-----")

for pattern in "${PATTERNS[@]}"; do
  if grep -rl "$pattern" "$BUNDLE_DIR" > /dev/null 2>&1; then
    echo "[ERROR] Found potential secret pattern '$pattern' in client bundle" >&2
    exit 1
  fi
done

echo "[OK] No known secret patterns found in $BUNDLE_DIR"

Für echte Secrets gilt bei Environment Variablen in React immer dieselbe Regel: sie dürfen nur in einer Umgebung existieren, die niemals zu Client-Code kompiliert wird, also in Server Components, API-Routen, Edge Functions oder einem separaten Backend. Ein React-Frontend darf Secrets höchstens indirekt über eine geschützte API nutzen, niemals direkt referenzieren.

6. Runtime-Konfiguration für Docker-Images

Ein besonders praktisches Problem bei Environment Variablen in React-Projekten entsteht, sobald ein Docker-Image für mehrere Umgebungen wie Staging und Production wiederverwendet werden soll. Ein Build pro Umgebung widerspricht dem Grundprinzip von Containern, dass genau dasselbe Image überall laufen soll. Die Lösung ist eine Runtime-Konfiguration, die Werte erst beim Containerstart injiziert, statt sie zur Build-Zeit fest einzubetten.

Dafür schreibt ein Entrypoint-Skript beim Containerstart die tatsächlichen Umgebungswerte in eine kleine JavaScript-Datei, die vor dem eigentlichen Bundle geladen wird und die Werte auf window ablegt.


#!/usr/bin/env sh
# docker-entrypoint.sh — inject runtime config before serving static files
set -eu

CONFIG_FILE="/usr/share/nginx/html/env-config.js"

cat <<EOF > "$CONFIG_FILE"
window.__ENV__ = {
  API_BASE_URL: "${API_BASE_URL:-https://api.mironsoft.de}",
  FEATURE_FLAGS_URL: "${FEATURE_FLAGS_URL:-}"
};
EOF

echo "[INFO] Runtime config written for this container instance"
exec nginx -g "daemon off;"

Im React-Code wird window.__ENV__.API_BASE_URL statt import.meta.env.VITE_API_BASE_URL gelesen, wodurch ein einziges Docker-Image für Staging und Production denselben Build nutzen kann. Dieser Ansatz kombiniert die Vorteile von Build-Time-Environment-Variablen für nicht sensible Konfiguration mit Runtime-Flexibilität dort, wo dieselbe Anwendung tatsächlich in mehreren Umgebungen laufen soll.

7. Typsicherheit für Environment Variablen mit Zod

Ohne Validierung führen fehlende oder falsch formatierte Environment Variablen in React-Projekten zu Fehlern, die erst zur Laufzeit im Browser sichtbar werden, oft weit entfernt von der eigentlichen Ursache. Zod erlaubt es, ein Schema für alle erwarteten Variablen zu definieren und beim Start des Builds sofort zu validieren, mit einer klaren Fehlermeldung statt eines kryptischen undefined irgendwo tief im Code.


// src/env.ts — validated, type-safe environment variables with Zod
import { z } from "zod";

const envSchema = z.object({
  VITE_API_BASE_URL: z.string().url(),
  VITE_SENTRY_DSN: z.string().url().optional(),
  VITE_MAX_UPLOAD_MB: z.coerce.number().positive().default(10),
});

// Throws a readable error immediately if something is missing or malformed
const parsed = envSchema.safeParse(import.meta.env);

if (!parsed.success) {
  console.error(parsed.error.flatten().fieldErrors);
  throw new Error("Invalid environment configuration — see console for details");
}

export const env = parsed.data;
// env.VITE_MAX_UPLOAD_MB is now a number, not a string

Dieser Ansatz macht Environment Variablen in React nicht nur typsicher für TypeScript, sondern auch selbstdokumentierend: das Schema selbst zeigt, welche Variablen erwartet werden, welchem Format sie folgen müssen und welche optional sind. Ein fehlgeschlagener Build wegen einer ungültigen URL ist deutlich einfacher zu debuggen als ein kaputtes Feature, das erst im Live-Betrieb auffällt.

8. CI/CD-Secrets-Management in der Praxis

Für Environment Variablen in React-Projekten muss auch die CI/CD-Pipeline selbst sauber mit Secrets umgehen. GitHub Actions bietet dafür Repository- und Environment-Secrets, die verschlüsselt gespeichert und in Workflow-Logs automatisch maskiert werden, sobald ihr Wert versehentlich ausgegeben würde. Vercel und Netlify bieten ein eigenes Dashboard für Environment Variablen mit getrennten Werten für Preview-, Development- und Production-Deployments.

Bei Docker-basierten Deployments sollten Secrets niemals über ENV-Anweisungen im Dockerfile gesetzt werden, weil diese im Image-Layer sichtbar bleiben, selbst wenn die Variable später überschrieben wird. Docker Secrets oder eine zur Laufzeit gemountete Datei sind der sicherere Weg, insbesondere in Kombination mit dem Runtime-Konfigurationsmuster aus dem vorherigen Abschnitt. So bleiben Environment Variablen in React-Deployments über den gesamten Lebenszyklus, vom lokalen Build bis zur Produktion, konsistent abgesichert.

9. Environment Variablen im Werkzeug-Vergleich

Die praktische Handhabung von Environment Variablen in React-Projekten unterscheidet sich zwischen Vite, Next.js und einer Runtime-Konfiguration in Docker deutlich. Die folgende Tabelle vergleicht die wichtigsten Eigenschaften.

Ansatz Präfix Zeitpunkt Für Secrets geeignet
Vite Client-Variable VITE_ Build Time Nein, öffentlich im Bundle
Next.js Server-Variable keins Server Runtime Ja, bleibt serverseitig
Next.js Client-Variable NEXT_PUBLIC_ Build Time Nein, öffentlich im Bundle
Runtime-Config (Docker) keins nötig Container-Start Nur nicht sensible Werte
CI/CD-Secret keins Build-Prozess Ja, verschlüsselt gespeichert

Die Tabelle macht deutlich, dass die Frage nach dem richtigen Umgang mit Environment Variablen in React nie pauschal beantwortet werden kann, sondern von der Sensibilität des Wertes und dem Zeitpunkt abhängt, zu dem er gebraucht wird. Nicht sensible Konfiguration wie eine API-Basis-URL kann bedenkenlos über VITE_ oder NEXT_PUBLIC_ eingebettet werden, während echte Secrets ausschließlich serverseitig oder in der CI/CD-Pipeline verbleiben dürfen.

Mironsoft

Sichere Konfiguration und Secrets Management für React Apps

Sind eure Environment Variablen wirklich sicher konfiguriert?

Wir prüfen eure React-Builds auf versehentlich eingebettete Secrets, führen typsichere Validierung mit Zod ein und bauen bei Bedarf eine Runtime-Konfiguration für eure Docker-Images.

Secrets-Audit

Bestehende Bundles auf versehentlich eingebettete Zugangsdaten prüfen

Validierung einführen

Zod-Schemas für Environment Variablen mit klaren Fehlermeldungen im Build

Runtime-Konfiguration

Ein Docker-Image für alle Umgebungen mit sauberer Injektion beim Start

10. Zusammenfassung

Environment Variablen in React unterscheiden sich fundamental vom Backend, weil sie zur Build-Zeit fest in den ausgelieferten Code eingebettet werden, sobald sie das Präfix VITE_ oder NEXT_PUBLIC_ tragen. Ein Wert, der auf dem Server geändert wird, hat ohne neuen Build keinerlei Wirkung. Diese Eigenschaft macht klar, warum echte Secrets niemals mit diesen Präfixen versehen werden dürfen, sondern ausschließlich serverseitig oder in einer separaten API-Schicht bleiben müssen.

Für Multi-Environment-Deployments über Docker löst eine Runtime-Konfiguration das Build-Time-Problem, indem Werte erst beim Containerstart injiziert werden. Typsichere Validierung mit Zod verhindert, dass fehlende oder fehlerhafte Variablen erst spät im Betrieb auffallen. Wer diese Prinzipien konsequent anwendet, vermeidet die häufigsten Sicherheitslücken und Debugging-Sackgassen im Umgang mit Environment Variablen in React-Projekten.

Environment Variablen in React Builds — Das Wichtigste auf einen Blick

Build Time versus Runtime

VITE_ und NEXT_PUBLIC_ Variablen werden fest in den Code eingebettet. Änderungen greifen erst nach einem neuen Build.

Secrets nie im Client

Alles mit Client-Präfix ist über die Browser-Entwicklertools öffentlich einsehbar. Echte Secrets bleiben serverseitig.

Runtime-Konfiguration

Ein Entrypoint-Skript injiziert Werte beim Containerstart, damit ein Docker-Image für mehrere Umgebungen reicht.

Typsicherheit mit Zod

Ein Schema validiert alle erwarteten Variablen beim Build und liefert klare Fehlermeldungen statt kryptischer undefined-Werte.

11. FAQ: Environment Variablen in React

1Warum wirkt eine Änderung nicht nach Redeploy?
Variablen werden zur Build Zeit eingebettet. Ein Redeploy ohne neuen Build nutzt weiterhin die alten Werte.
2Sind VITE_ Variablen öffentlich?
Ja, jeder Besucher kann sie über die Browser-Entwicklertools lesen. Niemals Secrets damit versehen.
3Warum funktioniert destructuring nicht?
Next.js erkennt nur direkten statischen Zugriff wie process.env.API_KEY, nicht Destructuring oder dynamische Schlüssel.
4Ein Image für mehrere Umgebungen?
Mit Runtime-Konfiguration: ein Entrypoint-Skript injiziert Werte beim Containerstart über window statt Build Time.
5Wofür eignet sich Zod?
Zod validiert alle Variablen gegen ein Schema beim Build und liefert klare Fehler statt spätem undefined.
6Was gehört in .env.example?
Alle Variablennamen ohne echte Werte, eingecheckt. Echte .env-Dateien mit Werten niemals einchecken.
7Sind ENV-Anweisungen im Dockerfile sicher?
Nein, sie bleiben im Image-Layer sichtbar. Docker Secrets oder gemountete Dateien sind der sicherere Weg.
8VITE_ versus NEXT_PUBLIC_?
Beide markieren Client-Variablen und werden zur Build Zeit eingebettet. Unterschied liegt nur im Framework.
9Wie Secret-Leaks im Bundle prüfen?
Ausgabeverzeichnis mit grep nach bekannten Geheimnis-Mustern durchsuchen und den Build bei einem Treffer abbrechen.
10Reicht ein Dashboard-Wechsel bei Vercel?
Nein, ein neuer Build muss ausgelöst werden, damit der geänderte Wert tatsächlich übernommen wird.