Cómo implementar variables de entorno de tiempo de ejecución con create-react-app, Docker y Nginx

Hay muchas formas de configurar su aplicación React. Usemos un enfoque que respete la metodología de la aplicación de doce factores. Esto significa que impone la reconfiguración durante el tiempo de ejecución. Por lo tanto, no se requeriría ninguna compilación por entorno.

? Que queremos lograr?

Queremos poder ejecutar nuestra aplicación React como un contenedor Docker que se compila una vez. Se ejecuta en todas partes al ser configurable durante el tiempo de ejecución. El resultado debe ser un contenedor liviano y de alto rendimiento que sirva a nuestra aplicación React como contenido estático, lo que logramos al usar Ngnix Alpine. Nuestra aplicación debería permitir la configuración dentro de un archivo docker-compose como este:

version: "3.2" services: my-react-app: image: my-react-app ports: - "3000:80" environment: - "API_URL=//production.example.com"

Deberíamos poder configurar nuestra aplicación React usando -eflag (variables de entorno) cuando usamos Docker runcommand.

A primera vista, este enfoque puede parecer un beneficio demasiado pequeño para el trabajo adicional que requiere para la configuración inicial. Pero una vez que se realiza la instalación, las configuraciones y la implementación específicas del entorno serán mucho más fáciles de manejar. Entonces, para cualquiera que se dirija a entornos dinámicos o que use sistemas de orquestación, este enfoque es definitivamente algo a considerar.

? El problema

En primer lugar, debe quedar claro que no existen variables de entorno dentro del entorno del navegador. Cualquiera que sea la solución que usemos hoy en día no es más que una falsa abstracción.

Pero, entonces, podría preguntar, ¿qué pasa con los .envarchivos y REACT_APPlas variables de entorno con prefijos que provienen directamente de la documentación? Incluso dentro del código fuente, estos se usan de la process.envmisma manera que usamos variables de entorno dentro de Node.js.

En realidad, el objeto processno existe dentro del entorno del navegador, es específico de un nodo. CRA de forma predeterminada no realiza la representación del lado del servidor. No puede inyectar variables de entorno durante la entrega de contenido (como lo hace Next.js). Durante la transpilación , el proceso de Webpack reemplaza todas las apariciones de process.envcon un valor de cadena que se proporcionó. Esto significa que solo se puede configurar durante el tiempo de compilación .

? Solución

El momento específico en el que todavía es posible inyectar variables de entorno ocurre cuando iniciamos nuestro contenedor. Entonces podemos leer las variables de entorno desde el interior del contenedor. Podemos escribirlos en un archivo que se puede servir a través de Nginx (que también sirve a nuestra aplicación React). Se importan a nuestra aplicación mediante una etiqueta dentro de la sección principal de index.html. Entonces, en ese momento, ejecutamos un script bash que crea un archivo JavaScript con variables de entorno asignadas como propiedades del windowobjeto global . Inyectado para estar disponible globalmente dentro de nuestra aplicación a través del navegador.

? Guía paso por paso

Comencemos con un create-react-appproyecto simple y .envcreemos un archivo con nuestra primera variable de entorno que queremos exponer.

# Generate React App create-react-app cra-runtime-environment-variables cd cra-runtime-environment-variables # Create default environment variables that we want to use touch .env echo "API_URL=https//default.dev.api.com" >> .env

Luego, escribamos un pequeño script bash que leerá el .envarchivo y extraerá las variables de entorno que se escribirán en el archivo. Si establece una variable de entorno dentro del contenedor, se utilizará su valor; de lo contrario, volverá al valor predeterminado del archivo .env. Creará un archivo JavaScript que coloca los valores de las variables de entorno como un objeto que se asigna como una propiedad del windowobjeto.

#!/bin/bash # Recreate config file rm -rf ./env-config.js touch ./env-config.js # Add assignment echo "window._env_ = {" >> ./env-config.js # Read each line in .env file # Each line represents key=value pairs while read -r line || [[ -n "$line" ]]; do # Split env variables by character `=` if printf '%s\n' "$line" | grep -q -e '='; then varname=$(printf '%s\n' "$line" | sed -e 's/=.*//') varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//') fi # Read value of current variable if exists as Environment variable value=$(printf '%s\n' "${!varname}") # Otherwise use value from .env file [[ -z $value ]] && value=${varvalue} # Append configuration property to JS file echo " $varname: \"$value\"," >> ./env-config.js done > ./env-config.js

Necesitamos agregar la siguiente línea al elemento dentro del index.htmlcual luego importa el archivo creado por nuestro script bash.

Vamos a mostrar nuestra variable de entorno dentro de la aplicación:

API_URL: {window._env_.API_URL}

? Desarrollo

Durante el desarrollo, si no queremos usar Docker, podemos ejecutar el script bash a través del npm scriptcorredor modificando package.json:

 "scripts": { "dev": "chmod +x ./env.sh && ./env.sh && cp env-config.js ./public/ && react-scripts start", "test": "react-scripts test", "eject": "react-scripts eject", "build": "react-scripts build'" },

Y si ejecutamos yarn devdeberíamos ver un resultado como este:

Hay dos formas de reconfigurar las variables de entorno dentro de dev. Cambie el valor predeterminado dentro del .envarchivo o anule los valores predeterminados ejecutando el yarn devcomando con las variables de entorno antepuestas:

API_URL=//my.new.dev.api.com yarn dev

Y finalmente, edite .gitignorepara excluir las configuraciones del entorno del código fuente:

# Temporary env files /public/env-config.js env-config.js

En cuanto al entorno de desarrollo, ¡eso es todo! Estamos a mitad de camino. No hemos hecho una gran diferencia en este momento en comparación con lo que CRA ofrece de forma predeterminada para el entorno de desarrollo. El verdadero potencial de este enfoque brilla en la producción.

? Producción

Ahora vamos a crear una configuración mínima de Nginx para que podamos construir una imagen optimizada que sirva a la aplicación lista para producción.

# Create directory for Ngnix configuration mkdir -p conf/conf.d touch conf/conf.d/default.conf conf/conf.d/gzip.conf

El archivo de configuración principal debería verse algo así:

server { listen 80; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; expires -1; # Set it to different value depending on your standard requirements } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }

También es útil habilitar la compresión gzip para que nuestros activos sean más livianos durante la transición de red:

gzip on; gzip_http_version 1.0; gzip_comp_level 5; # 1-9 gzip_min_length 256; gzip_proxied any; gzip_vary on; # MIME-types gzip_types application/atom+xml application/javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component;

Ahora que nuestra configuración de Nginx está lista, finalmente podemos crear archivos Dockerfile y docker-compose:

touch Dockerfile docker-compose.yml

Inicialmente, usamos la node:alpineimagen para crear una construcción de producción optimizada de nuestra aplicación. Luego, construimos una imagen en tiempo de ejecución encima de nginx:alpine.

# => Build container FROM node:alpine as builder WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn COPY . . RUN yarn build # => Run container FROM nginx:1.15.2-alpine # Nginx config RUN rm -rf /etc/nginx/conf.d COPY conf /etc/nginx # Static build COPY --from=builder /app/build /usr/share/nginx/html/ # Default port exposure EXPOSE 80 # Copy .env file and shell script to container WORKDIR /usr/share/nginx/html COPY ./env.sh . COPY .env . # Add bash RUN apk add --no-cache bash # Make our shell script executable RUN chmod +x env.sh # Start Nginx server CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]

Ahora nuestro contenedor está listo. Podemos hacer todas las cosas estándar con él. Podemos construir un contenedor, ejecutarlo con configuraciones en línea y enviarlo a un repositorio proporcionado por servicios como Dockerhub.

docker build . -t kunokdev/cra-runtime-environment-variables docker run -p 3000:80 -e API_URL=//staging.api.com -t kunokdev/cra-runtime-environment-variables docker push -t kunokdev/cra-runtime-environment-variables

El docker runcomando anterior debería generar una aplicación así:

Por último, creemos nuestro archivo docker-compose. Por lo general, tendrá diferentes archivos de composición acoplable según el entorno y usará la -fbandera para seleccionar qué archivo usar.

version: "3.2" services: cra-runtime-environment-variables: image: kunokdev/cra-runtime-environment-variables ports: - "5000:80" environment: - "API_URL=production.example.com"

Y si lo hacemos docker-compose up, deberíamos ver una salida así:

¡Excelente! Ahora hemos logrado nuestro objetivo. Podemos reconfigurar nuestra aplicación fácilmente tanto en entornos de desarrollo como de producción de una manera muy conveniente. ¡Ahora finalmente podemos construir solo una vez y ejecutar en todas partes!

Si se atascó o tiene ideas adicionales, acceda al código fuente en GitHub.

? Próximos pasos

La implementación actual del script de shell imprimirá todas las variables incluidas en el archivo .env. La mayoría de las veces no queremos exponerlos todos. Puede implementar filtros para las variables que no desea exponer utilizando prefijos o una técnica similar.

? Soluciones alternativas

As noted above, the build time configuration will satisfy most use cases. You can rely on the default approach using .env file per environment and build a container for each environment and inject values via CRA Webpack provided environment variables.

You could also have a look at this CRA GitHub repository issue which covers this problem. By now, there should be more posts and issues which cover this topic. Each offers a similar solution as above. It’s up to you to decide how are you going to implement specific details. You might use Node.js to serve your application which means that you can also replace shells script with Node.js script. Note that Nginx is more convenient to serve static content.

If you have any questions or want to offer feedback; feel free to open issue on GitHub. Optionally follow me for further posts related to web technologies.