5 min read

Como implementar variáveis ​​de ambiente com ReactJS, Docker e Nginx

Como implementar variáveis ​​de ambiente com ReactJS, Docker e Nginx

Hoje encontrei uma problema ao realizar o deploy de uma aplicação da Overall.Cloud em diversos servidores. Para você entender o escopo do problema, temos uma aplicação em ReactJS com MySQL e servidor Nginx – utilizando AWS. Como realizamos diversos deploys, preciso automatizar o processo. Então como fazer isso de forma rápida e fácil?

Para implementar o Docker precisei enviar parâmetros dinâmicos para o Nginx, como domínio do cliente, URL de API, etc…

O que eu preciso

Quero executar nosso aplicativo React como um contêiner do Docker que é construído uma vez. Ele roda em qualquer lugar sendo configurável durante o tempo de execução. A saída deve ser um contêiner leve e de alto desempenho que serve nosso aplicativo React como conteúdo estático, o que conseguimos usando o Ngnix Alpine. 

Nosso aplicativo deve permitir a configuração no arquivo docker-compose como este:

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

Devemos ser capazes de configurar nosso aplicativo React usando a flag -e (variáveis ​​de ambiente) ao usar o comando Docker run.

O problema

Em primeiro lugar, deve ficar claro que não existem variáveis ​​de ambiente dentro do ambiente do navegador. Qualquer que seja a solução que usamos hoje em dia não passa de uma falsa abstração.

Mas, então você pode perguntar, e quanto as variáveis .envREACT_APP ​​de ambiente prefixadas que vêm diretamente da documentação? Mesmo dentro do código-fonte, os process.env são usados  ​da mesma forma que usamos variáveis ​​de ambiente dentro do Node.js.

Na realidade, o objeto process não existe dentro do ambiente do navegador, é específico do Node. O CRA por padrão não faz renderização do lado do servidor. Ele não pode injetar variáveis ​​de ambiente durante a veiculação de conteúdo (como o Next.js faz). Durante a transpilação, o processo Webpack substitui todas as ocorrências de process.env por um valor de string que foi fornecido. Isso significa que ele só pode ser configurado durante o tempo de construção.

Guia passo a passo

Vamos começar com um create-react-app simples e criar um arquivo .env um com nossa primeira variável de ambiente que queremos expor.

# Gera React-App
create-react-app cra-runtime-environment-variables
cd cra-runtime-environment-variables

# Cria variáveis padrões para usar
touch .env
echo "API_URL=https//default.dev.api.com" >> .env

Então vamos escrever um pequeno script bash que lerá .env e extrairá as variáveis ​​de ambiente que serão gravadas no arquivo. Se você definir uma variável de ambiente dentro do contêiner, seu valor será usado, caso contrário, retornará ao valor padrão do arquivo .env. Ele criará um arquivo JavaScript que coloca os valores das variáveis ​​de ambiente como um objeto que é atribuído como uma propriedade do objeto window.

#!/bin/bash

# Recria o arquivo de config
rm -rf ./env-config.js
touch ./env-config.js

# Adiciona a tarefa 
echo "window._env_ = {" >> ./env-config.js

# Lê o arquivo .env
while read -r line || [[ -n "$line" ]];
do
  # Dividi variáveis de ambiente por caractere `=`
  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

  # Lê o valor da variável atual se existir como variável de ambiente
  value=$(printf '%s\n' "${!varname}")
  # Caso contrário, use o valor do arquivo .env
  [[ -z $value ]] && value=${varvalue}
  
  # Adiciona as propriedades ao arquivo JS
  echo "  $varname: \"$value\"," >> ./env-config.js
done < .env

echo "}" >> ./env-config.js

Precisamos adicionar a seguinte linha ao <head> elemento dentro index.html do qual importa o arquivo criado pelo nosso script bash.

<script src="%PUBLIC_URL%/env-config.js"></script>

index.html

Vamos exibir nossa variável de ambiente dentro do aplicativo:

<p>API_URL: {window._env_.API_URL}</p>

src/App.js

Desenvolvimento

Durante o desenvolvimento, se não quisermos usar o Docker, podemos executar o script bash via npm script 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'"
  },

E se executarmos nrpm start, devemos ver uma saída válida.

Alterando as variáveis

Existem duas maneiras de reconfigurar variáveis ​​de ambiente no dev. Altere o valor padrão dentro .env do arquivo ou substitua os padrões executando o yarn dev comando com variáveis ​​de ambiente prefixadas:

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

E, finalmente, edite .gitignore para excluirmos as configurações de ambiente do código-fonte:

# Arquivos env temporários
/public/env-config.js
env-config.js

Quanto ao ambiente de desenvolvimento, é isso! Estamos no meio caminho. Não fizemos uma grande diferença neste momento em comparação com o que o CRA oferecia por padrão para o ambiente de desenvolvimento. O verdadeiro potencial dessa abordagem brilha na produção.

Produção

Agora vamos criar uma configuração mínima do Nginx para que possamos construir uma imagem otimizada que atenda ao aplicativo pronto para produção.

# Cria diretório padrão para configurações Nginx
mkdir -p conf/conf.d
touch conf/conf.d/default.conf conf/conf.d/gzip.conf

O arquivo conf/conf.d/default.conf de configuração principal deve ficar assim:

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;
  }
}

Também é útil habilitar a compactação gzip para que nossos ativos sejam mais leves durante a transição de rede, edite o conf/conf.d/gzip.conf:

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;

Agora que nossa configuração do Nginx está pronta, podemos finalmente criar os arquivos Dockerfile e docker-compose:

touch Dockerfile docker-compose.yml

Inicialmente, usamos a imagem node:alpine para criar uma compilação de produção otimizada do nosso aplicativo. Em seguida, criamos uma imagem de tempo de execução em cima do nginx:alpine.

FROM node:alpine as builder
WORKDIR /app
COPY package.json .
COPY yarn.lock .
RUN yarn
COPY . .
RUN yarn build

FROM nginx:1.15.2-alpine

RUN rm -rf /etc/nginx/conf.d
COPY conf /etc/nginx

COPY --from=builder /app/build /usr/share/nginx/html/

EXPOSE 80

WORKDIR /usr/share/nginx/html
COPY ./env.sh .
COPY .env .

RUN apk add --no-cache bash

RUN chmod +x env.sh

CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]

Agora nosso container está pronto. Podemos fazer todas as coisas padrão com ele. Podemos construir um contêiner, executá-lo com configurações inline:

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

O comando docker run deve gerar o aplicativo.

Por fim, vamos criar nosso arquivo docker-compose. Normalmente, você terá diferentes arquivos de composição do docker dependendo do ambiente e usará o sinalizador -f para selecionar qual arquivo usar.

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

Excelente! Agora atingimos nosso objetivo. Podemos reconfigurar nosso aplicativo facilmente em ambientes de desenvolvimento e produção de uma maneira muito fácil. Agora podemos finalmente construir apenas uma vez e rodar em todos os lugares!