Runtime Environment Variables for React Apps with Nginx and Docker

Runtime Environment Variables for React Apps with Nginx and Docker

Source: Dev.to

How it works ## Add public/runtime-env.template.js ## In index.html, add in <head>: ## In your code: ## Dockerfile ## entrypoint.sh ## Why this is useful ## How it works ## Result React environment variables are normally injected at build time. Once the app is built, changing API URLs, feature flags, or monitoring config requires rebuilding the image. This becomes a problem when the same Docker image needs to run in staging, production, or other environments. This setup allows runtime configuration for React apps served by Nginx in Docker. The image is built once and configured when the container starts. Done. Container generates the config file on startup from env vars. Safe because we only allow specific ones with envsubst. With this approach, the frontend behaves more like a backend service: It avoids the common issue where frontend config is tightly coupled to the build step. A small JavaScript template is added to the public folder. At container startup, Nginx runs a script that replaces placeholders in this file with actual environment variable values using envsubst. The generated file is then loaded before the React app and exposes the config through window.RUNTIME_ENV. Only variables explicitly listed in the template are exposed, reducing the risk of leaking unintended values. You get true runtime configuration for a static React app, without rebuilding the image. The same Docker image can safely run in different environments with different settings, using standard environment variables. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: window.RUNTIME_ENV = { API_URL: "${API_URL}", FEATURE_FLAG_ANALYTICS: "${FEATURE_FLAG_ANALYTICS}", FEATURE_FLAG_NEW_DASHBOARD: "${FEATURE_FLAG_NEW_DASHBOARD}", SENTRY_DSN: "${SENTRY_DSN}", }; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: window.RUNTIME_ENV = { API_URL: "${API_URL}", FEATURE_FLAG_ANALYTICS: "${FEATURE_FLAG_ANALYTICS}", FEATURE_FLAG_NEW_DASHBOARD: "${FEATURE_FLAG_NEW_DASHBOARD}", SENTRY_DSN: "${SENTRY_DSN}", }; CODE_BLOCK: window.RUNTIME_ENV = { API_URL: "${API_URL}", FEATURE_FLAG_ANALYTICS: "${FEATURE_FLAG_ANALYTICS}", FEATURE_FLAG_NEW_DASHBOARD: "${FEATURE_FLAG_NEW_DASHBOARD}", SENTRY_DSN: "${SENTRY_DSN}", }; CODE_BLOCK: <script src="/runtime-env.js"></script> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <script src="/runtime-env.js"></script> CODE_BLOCK: <script src="/runtime-env.js"></script> CODE_BLOCK: const config = { API_URL: 'https://fallback.com', FEATURE_FLAG_ANALYTICS: false, // defaults ...(window.RUNTIME_ENV || {}), }; export default config; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const config = { API_URL: 'https://fallback.com', FEATURE_FLAG_ANALYTICS: false, // defaults ...(window.RUNTIME_ENV || {}), }; export default config; CODE_BLOCK: const config = { API_URL: 'https://fallback.com', FEATURE_FLAG_ANALYTICS: false, // defaults ...(window.RUNTIME_ENV || {}), }; export default config; COMMAND_BLOCK: # Build FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build ### Runtime FROM nginx:alpine RUN apk add --no-cache gettext COPY --from=builder /app/build /usr/share/nginx/html # /dist for Vite COPY public/runtime-env.template.js /usr/share/nginx/html/runtime-env.template.js COPY entrypoint.sh /docker-entrypoint.d/40-runtime-env.sh RUN chmod +x /docker-entrypoint.d/40-runtime-env.sh CMD ["nginx", "-g", "daemon off;"] Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Build FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build ### Runtime FROM nginx:alpine RUN apk add --no-cache gettext COPY --from=builder /app/build /usr/share/nginx/html # /dist for Vite COPY public/runtime-env.template.js /usr/share/nginx/html/runtime-env.template.js COPY entrypoint.sh /docker-entrypoint.d/40-runtime-env.sh RUN chmod +x /docker-entrypoint.d/40-runtime-env.sh CMD ["nginx", "-g", "daemon off;"] COMMAND_BLOCK: # Build FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build ### Runtime FROM nginx:alpine RUN apk add --no-cache gettext COPY --from=builder /app/build /usr/share/nginx/html # /dist for Vite COPY public/runtime-env.template.js /usr/share/nginx/html/runtime-env.template.js COPY entrypoint.sh /docker-entrypoint.d/40-runtime-env.sh RUN chmod +x /docker-entrypoint.d/40-runtime-env.sh CMD ["nginx", "-g", "daemon off;"] COMMAND_BLOCK: #!/bin/sh set -eu export API_URL=${API_URL:-} export FEATURE_FLAG_ANALYTICS=${FEATURE_FLAG_ANALYTICS:-} export FEATURE_FLAG_NEW_DASHBOARD=${FEATURE_FLAG_NEW_DASHBOARD:-} export SENTRY_DSN=${SENTRY_DSN:-} envsubst '${API_URL} ${FEATURE_FLAG_ANALYTICS} ${FEATURE_FLAG_NEW_DASHBOARD} ${SENTRY_DSN}' \ < /usr/share/nginx/html/runtime-env.template.js \ > /usr/share/nginx/html/runtime-env.js rm /usr/share/nginx/html/runtime-env.template.js exec "$@" Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: #!/bin/sh set -eu export API_URL=${API_URL:-} export FEATURE_FLAG_ANALYTICS=${FEATURE_FLAG_ANALYTICS:-} export FEATURE_FLAG_NEW_DASHBOARD=${FEATURE_FLAG_NEW_DASHBOARD:-} export SENTRY_DSN=${SENTRY_DSN:-} envsubst '${API_URL} ${FEATURE_FLAG_ANALYTICS} ${FEATURE_FLAG_NEW_DASHBOARD} ${SENTRY_DSN}' \ < /usr/share/nginx/html/runtime-env.template.js \ > /usr/share/nginx/html/runtime-env.js rm /usr/share/nginx/html/runtime-env.template.js exec "$@" COMMAND_BLOCK: #!/bin/sh set -eu export API_URL=${API_URL:-} export FEATURE_FLAG_ANALYTICS=${FEATURE_FLAG_ANALYTICS:-} export FEATURE_FLAG_NEW_DASHBOARD=${FEATURE_FLAG_NEW_DASHBOARD:-} export SENTRY_DSN=${SENTRY_DSN:-} envsubst '${API_URL} ${FEATURE_FLAG_ANALYTICS} ${FEATURE_FLAG_NEW_DASHBOARD} ${SENTRY_DSN}' \ < /usr/share/nginx/html/runtime-env.template.js \ > /usr/share/nginx/html/runtime-env.js rm /usr/share/nginx/html/runtime-env.template.js exec "$@" COMMAND_BLOCK: docker run -p 80:80 \ -e API_URL=https://api.prod.com \ -e FEATURE_FLAG_ANALYTICS=true \ your-image:latest Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: docker run -p 80:80 \ -e API_URL=https://api.prod.com \ -e FEATURE_FLAG_ANALYTICS=true \ your-image:latest COMMAND_BLOCK: docker run -p 80:80 \ -e API_URL=https://api.prod.com \ -e FEATURE_FLAG_ANALYTICS=true \ your-image:latest - One Docker image can be reused across environments - API URLs, feature flags, and DSNs can change without rebuild - Works well with Docker, Kubernetes, and CI/CD pipelines - Configuration is explicit and controlled