Skip to content

Instantly share code, notes, and snippets.

@renatoapcosta
Last active February 10, 2026 02:04
Show Gist options
  • Select an option

  • Save renatoapcosta/4e5a72533e65e7e4bbbc262714d8dfa5 to your computer and use it in GitHub Desktop.

Select an option

Save renatoapcosta/4e5a72533e65e7e4bbbc262714d8dfa5 to your computer and use it in GitHub Desktop.

Abaixo vai um template com boas práticas para servir um build React com nginx:1.29.4-alpine, sem root em runtime, com logs acessíveis, e com config por variáveis de ambiente para apontar para um backend externo.


Objetivo técnico

Situação: React gera arquivos estáticos (build/), Nginx serve esses arquivos e faz proxy para um backend (/api) que pode estar fora do compose.

Ação:

Multi-stage build: Node compila → Nginx serve.

Nginx roda como usuário não-root (UID/GID fixos).

Logs vão para stdout/stderr (melhor prática de container) e, opcionalmente, também para arquivos com volume.

Variáveis de ambiente são injetadas em runtime via envsubst para gerar config do Nginx.

docker-compose.yml com read-only filesystem, tmpfs, drop de capabilities, healthcheck.

Resultado: container mais seguro, observável e fácil de configurar.


Estrutura sugerida

. ├─ Dockerfile ├─ docker-compose.yml └─ nginx/ ├─ default.conf.template └─ 50-envsubst.sh


Dockerfile (multi-stage + non-root)

---------- build stage ----------

FROM node:20-alpine AS build WORKDIR /app

Instala deps primeiro (melhor cache)

COPY package*.json ./ RUN npm ci

Copia o resto e builda

COPY . . RUN npm run build

---------- runtime stage ----------

FROM nginx:1.29.4-alpine

UID/GID fixos (evita surpresas em volumes/hosts)

ARG APP_UID=10101 ARG APP_GID=10101

Cria usuário/grupo e prepara diretórios com permissão

RUN addgroup -g ${APP_GID} -S app
&& adduser -u ${APP_UID} -S -G app -H -s /sbin/nologin app
&& mkdir -p /var/cache/nginx /var/run /var/log/nginx /etc/nginx/templates
&& chown -R app:app /var/cache/nginx /var/run /var/log/nginx /etc/nginx
&& rm -rf /usr/share/nginx/html/*

Copia build do React

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

Template do nginx e script para envsubst

COPY nginx/default.conf.template /etc/nginx/templates/default.conf.template COPY nginx/50-envsubst.sh /docker-entrypoint.d/50-envsubst.sh RUN chmod +x /docker-entrypoint.d/50-envsubst.sh
&& chown -R app:app /docker-entrypoint.d /etc/nginx/templates

Expõe porta alta para não precisar de root (>=1024)

EXPOSE 8080

Healthcheck básico (sem curl)

HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1

Roda como não-root

USER app

Mantém foreground

CMD ["nginx", "-g", "daemon off;"]

Por que porta 8080? Para evitar capabilities/privileged port (80) e manter runtime não-root sem gambiarras.


Nginx template com env vars + SPA + proxy /api

nginx/default.conf.template

server { listen 8080; server_name _;

root /usr/share/nginx/html; index index.html;

Logs: para container, prefira stdout/stderr

access_log /dev/stdout; error_log /dev/stderr warn;

Health endpoint simples

location = /healthz { add_header Content-Type text/plain; return 200 "ok\n"; }

Cache agressivo p/ assets versionados

location ~* .(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 30d; add_header Cache-Control "public, max-age=2592000, immutable"; try_files $uri =404; }

SPA fallback

location / { try_files $uri /index.html; }

Proxy para backend externo (variáveis via envsubst)

location /api/ { proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_connect_timeout 5s;
proxy_read_timeout 60s;

# Ex:
# BACKEND_URL=http://meu-backend.externo:8080
proxy_pass ${BACKEND_URL};

} }

Observação: proxy_pass ${BACKEND_URL}; funciona bem se você definir BACKEND_URL com o host/porta e terminar com / quando necessário.

Exemplo seguro:

BACKEND_URL=http://host:8080/ (com barra final)

Assim /api/foo vira http://host:8080/foo (se quiser manter /api, aí você ajusta o proxy_pass + rewrite).


Script para gerar conf a partir das env vars

nginx/50-envsubst.sh

#!/bin/sh set -eu

: "${BACKEND_URL:=http://localhost:8081/}"

envsubst '${BACKEND_URL}'
< /etc/nginx/templates/default.conf.template \

/etc/nginx/conf.d/default.conf


docker-compose.yml (seguro por padrão)

services: web: build: context: . dockerfile: Dockerfile container_name: react-nginx ports: - "8080:8080" environment: # backend pode ser externo (DNS/hostname/IP) # exemplos: # - http://api.meudominio.com/ # - http://10.0.0.10:9000/ BACKEND_URL: "http://api.externo.local:9000/" restart: unless-stopped

# Boas práticas hardening
read_only: true
tmpfs:
  - /tmp
  - /var/run
  - /var/cache/nginx
security_opt:
  - no-new-privileges:true
cap_drop:
  - ALL

# Se você precisa de arquivos de log (geralmente não precisa):
# volumes:
#   - nginx_logs:/var/log/nginx

volumes:

nginx_logs:

Sobre “logs com permissão nas pastas”

Melhor prática em container: logar para stdout/stderr e coletar via Docker/compose/stack (ELK, Loki, CloudWatch etc.). Isso evita dor de cabeça com permissões e rotação.

Se sua exigência é arquivo de log, então o volume nginx_logs resolve e, como o usuário app é dono de /var/log/nginx, ele consegue escrever sem root.


React: como passar BACKEND para o frontend (quando precisa)

Se o React precisa saber o backend (ex: chamadas feitas no browser), duas abordagens comuns:

  1. Browser chama o mesmo host e usa /api (recomendado)

No código React, use /api como base.

Quem decide o destino real é o Nginx via BACKEND_URL.

  1. Injetar runtime config no React (se você não quer proxy)

Criar um env.js gerado no entrypoint com variáveis e carregar no index.html. Se você quiser, eu te passo esse padrão também.


Checklist rápido de segurança (o que já está atendido)

Runtime não-root (USER app) ✅

Porta não privilegiada (8080) ✅

read_only: true + tmpfs para paths que precisam escrita ✅

no-new-privileges ✅

cap_drop: [ALL] ✅

Logs para stdout/stderr ✅


Se você me disser:

se seu build React sai em build/ (CRA) ou dist/ (Vite),

e se você quer que /api mantenha o prefixo ou remova,

eu ajusto o proxy_pass/rewrite certinho para não quebrar rotas.


Correto. Se o build do React já é feito fora do Docker, multi-stage não é obrigatório.

Mas há implicações claras. Vou separar de forma objetiva.


Quando NÃO usar multi-stage (seu caso)

Situação

Você roda npm run build na sua máquina.

A pasta build/ (ou dist/) já existe.

A imagem só precisa servir arquivos estáticos.

Ação

Dockerfile simples, só com nginx.

Copia apenas o resultado do build.

Resultado

Build mais rápido.

Dockerfile mais simples.

Menos dependências dentro da imagem.

👉 Totalmente válido e comum em pipelines.


Dockerfile sem multi-stage (boas práticas mantidas)

FROM nginx:1.29.4-alpine

ARG APP_UID=10101 ARG APP_GID=10101

RUN addgroup -g ${APP_GID} -S app
&& adduser -u ${APP_UID} -S -G app -H -s /sbin/nologin app
&& mkdir -p /var/cache/nginx /var/run /var/log/nginx /etc/nginx/templates
&& chown -R app:app /var/cache/nginx /var/run /var/log/nginx /etc/nginx
&& rm -rf /usr/share/nginx/html/*

Copia SOMENTE o build já gerado localmente

COPY build/ /usr/share/nginx/html/

Nginx com env vars

COPY nginx/default.conf.template /etc/nginx/templates/default.conf.template COPY nginx/50-envsubst.sh /docker-entrypoint.d/50-envsubst.sh

RUN chmod +x /docker-entrypoint.d/50-envsubst.sh
&& chown -R app:app /docker-entrypoint.d /etc/nginx/templates

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --retries=3
CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1

USER app

CMD ["nginx", "-g", "daemon off;"]


O que você ganha ao evitar multi-stage

✔ Imagem menor (não carrega Node) ✔ Build previsível (feito local/CI) ✔ Dockerfile mais direto ✔ Menos superfície de ataque


O que você perde

⚠ Build não é reprodutível só com docker build ⚠ Precisa garantir que build/ esteja atualizado ⚠ Dependência do ambiente da máquina ou CI

Em ambientes corporativos, isso normalmente é aceito porque o build já ocorre antes do deploy.


Regra prática (boa para equipe)

Use isso como padrão mental:

Dev local / CI já gera build → sem multi-stage

Imagem deve ser 100% autossuficiente → multi-stage

Kubernetes + GitOps → geralmente multi-stage

Legacy / pipeline simples → sem multi-stage


docker-compose.yml (não muda)

O docker-compose.yml continua idêntico ao que passei antes.


Se quiser, posso:

validar seu .gitignore para evitar subir build/ errado

adaptar isso para Vite

endurecer ainda mais (SELinux, rootfs imutável total)

Só me diga qual cenário você quer fechar como padrão.


Sim, dá para aproveitar. Só tem 2 pegadinhas que você precisa controlar: porta, paths graváveis e como o Nginx é iniciado.


  1. O usuário nginx já existe, mas o container pode iniciar como root

Na imagem oficial, é comum o processo master iniciar como root para abrir porta 80 e depois “dropar” para o usuário nginx nos workers (via user nginx; no nginx.conf).

Se você quer garantia de “não-root” no container, você deve:

usar porta alta (ex: 8080), e

definir USER nginx no Dockerfile (ou user: nginx no compose),

garantir que os diretórios que o Nginx precisa escrever sejam graváveis por nginx.


  1. O que o Nginx precisa escrever

Mesmo servindo estático, o Nginx normalmente escreve em:

/var/cache/nginx (cache/temp)

/var/run (pid)

opcionalmente /var/log/nginx (se log em arquivo)

Se você usar read_only: true no compose (boa prática), esses diretórios precisam ser tmpfs ou volume, e com permissão para o usuário nginx.


  1. Dockerfile usando USER nginx (sem criar usuário novo)

Exemplo bem direto (assumindo build já feito local em build/):

FROM nginx:1.29.4-alpine

Limpa html default e copia seu build

RUN rm -rf /usr/share/nginx/html/* COPY build/ /usr/share/nginx/html/

Template + envsubst (se você usar variáveis de ambiente no nginx conf)

COPY nginx/default.conf.template /etc/nginx/templates/default.conf.template COPY nginx/50-envsubst.sh /docker-entrypoint.d/50-envsubst.sh RUN chmod +x /docker-entrypoint.d/50-envsubst.sh

Garante permissão nos diretórios que precisam escrita

(nginx user já existe, então só ajusta ownership/perms)

RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx /etc/nginx/templates
&& chown -R nginx:nginx /var/cache/nginx /var/run /var/log/nginx /etc/nginx/templates /usr/share/nginx/html

Porta alta para não precisar root

EXPOSE 8080

Rodar como usuário nginx

USER nginx

CMD ["nginx", "-g", "daemon off;"]

✅ Isso evita o problema do “UID 10101 vs host”. ⚠ Só lembre: se você bind-mount um diretório do host para /var/log/nginx, volta a questão de permissão do host.


  1. docker-compose.yml seguro com USER nginx

services: web: build: . ports: - "8080:8080" environment: BACKEND_URL: "http://api.externo.local:9000/" restart: unless-stopped

# Hardening
user: "nginx"
read_only: true
tmpfs:
  - /tmp
  - /var/run
  - /var/cache/nginx
security_opt:
  - no-new-privileges:true
cap_drop:
  - ALL

# Se você realmente quiser log em arquivo:
# volumes:
#   - nginx_logs:/var/log/nginx

volumes:

nginx_logs:

Log em arquivo vs stdout

Recomendado: access_log /dev/stdout; e error_log /dev/stderr;

Se insistir em arquivo: prefira volume nomeado (evita UID/GID do host)


  1. O ponto mais importante: sua config deve escutar 8080

No default.conf.template:

server { listen 8080; ... }

Se você deixar listen 80;, com USER nginx, o Nginx não sobe (porta privilegiada).


Recomendação objetiva

Sim, aproveite o usuário nginx.

Mantenha porta 8080, logs em stdout/stderr, read_only + tmpfs.

Use volume nomeado só se realmente precisar de arquivo no disco.

Se você colar aqui seu default.conf atual (ou template), eu ajusto ele para:

SPA fallback correto

proxy /api sem quebrar path

envsubst seguro (sem vazar variáveis desnecessárias)


Sim — pode ser interessante, mas depende do que você quer controlar.

Quando 1 arquivo único ajuda

Situação: você quer empacotar toda a configuração (events/http/server) em um lugar só, versionado com a app. Ação: você fornece um nginx.conf completo e não usa conf.d/default.conf. Resultado: menos “mágica” do container e menos arquivos espalhados.

Quando manter nginx.conf + conf.d/*.conf é melhor

Situação: você quer variar só o “server” (rotas, proxy, SPA) e reaproveitar defaults do Nginx. Ação: deixa nginx.conf padrão e mexe só em conf.d/default.conf. Resultado: mudanças menores e menor risco de quebrar defaults importantes.


Melhor prática que eu recomendo aqui

1 arquivo único para o “site”: conf.d/default.conf (ou template).

Deixe o nginx.conf base da imagem (a menos que você precise mudar events, worker_processes, log_format, gzip, etc.).

Isso dá equilíbrio: simples e seguro.


Se você quiser MESMO um único nginx.conf (completo)

Você consegue, e fica limpo. Exemplo pronto para SPA + proxy + não-root + logs stdout/stderr.

nginx/nginx.conf.template

user nginx; worker_processes auto;

pid /var/run/nginx.pid;

events { worker_connections 1024; }

http { include /etc/nginx/mime.types; default_type application/octet-stream;

access_log /dev/stdout; error_log /dev/stderr warn;

sendfile on; keepalive_timeout 65;

server { listen 8080; server_name _;

root /usr/share/nginx/html;
index index.html;

location = /healthz {
  add_header Content-Type text/plain;
  return 200 "ok\n";
}

location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
  expires 30d;
  add_header Cache-Control "public, max-age=2592000, immutable";
  try_files $uri =404;
}

location / {
  try_files $uri /index.html;
}

location /api/ {
  proxy_http_version 1.1;

  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;

  proxy_connect_timeout 5s;
  proxy_read_timeout 60s;

  proxy_pass ${BACKEND_URL};
}

} }

Script nginx/50-envsubst.sh (gera o nginx.conf)

#!/bin/sh set -eu

: "${BACKEND_URL:=http://localhost:8081/}"

envsubst '${BACKEND_URL}'
< /etc/nginx/templates/nginx.conf.template \

/etc/nginx/nginx.conf

Dockerfile (usando nginx user da imagem)

FROM nginx:1.29.4-alpine

RUN rm -rf /usr/share/nginx/html/* COPY build/ /usr/share/nginx/html/

COPY nginx/nginx.conf.template /etc/nginx/templates/nginx.conf.template COPY nginx/50-envsubst.sh /docker-entrypoint.d/50-envsubst.sh RUN chmod +x /docker-entrypoint.d/50-envsubst.sh

RUN mkdir -p /var/cache/nginx /var/run /etc/nginx/templates
&& chown -R nginx:nginx /var/cache/nginx /var/run /etc/nginx /usr/share/nginx/html

EXPOSE 8080 USER nginx CMD ["nginx", "-g", "daemon off;"]

Compose (igual, com hardening)

services: web: build: . ports: - "8080:8080" environment: BACKEND_URL: "http://api.externo.local:9000/" read_only: true tmpfs: - /tmp - /var/run - /var/cache/nginx security_opt: - no-new-privileges:true cap_drop: - ALL


Pontos de atenção (para não se surpreender)

Se você usa USER nginx, sempre use porta 8080 (ou outra >1024).

Se você usar read_only: true, precisa de tmpfs para /var/run e /var/cache/nginx.

Eu recomendo stdout/stderr para logs. Só use arquivo se tiver requisito explícito.


Se você me disser se seu React é CRA (build/) ou Vite (dist/) e se o backend precisa manter o prefixo /api, eu ajusto o proxy_pass para ficar 100% correto (sem duplicar path ou cortar demais).

#!/bin/sh
set -eu

# Defaults (se você quiser manter)
APP_BASE_URL="${BASE_URL:-http://localhost:8080}"
APP_API_URL_BFF="${API_URL_BFF:-http://localhost:8080/servecel/api/v1/}"

# Monta "window.env = {...};" com todas as APP_ do ambiente
# Filtra apenas variáveis começando com APP_
# Escapa \ e " para não quebrar o JS
json_pairs=""

while IFS='=' read -r key value; do
  # segurança: ignora se vier vazio
  [ -n "${key:-}" ] || continue

  # escapa para string JS
  esc_value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')

  # monta lista "K":"V",
  if [ -z "$json_pairs" ]; then
    json_pairs=$(printf '"%s":"%s"' "$key" "$esc_value")
  else
    json_pairs=$(printf '%s,"%s":"%s"' "$json_pairs" "$key" "$esc_value")
  fi
done <<EOF
$(env | grep '^APP_' || true)
EOF

env_config=$(printf 'window.env={%s};' "$json_pairs")

echo "Generating env-config.js with the following configuration:"
echo "$env_config"

printf '%s\n' "$env_config" | tee /usr/share/nginx/html/env-config.js >/dev/null
#!/bin/sh
set -eu

OUT_DIR="/tmp/runtime-config"
OUT_FILE="$OUT_DIR/env-config.js"

# 1) allowlist (só essas vão para o browser)
ALLOWED_VARS="
APP_BASE_URL
APP_API_URL
APP_AUTH_URL
APP_ENV
APP_VERSION
"

# 2) obrigatórias
REQUIRED_VARS="
APP_BASE_URL
APP_API_URL
"

mkdir -p "$OUT_DIR"

missing=""
for var in $REQUIRED_VARS; do
  # shellcheck disable=SC2039
  if [ -z "${!var:-}" ]; then
    missing="$missing $var"
  fi
done

if [ -n "$missing" ]; then
  echo "[ERROR] Missing required environment variables:$missing" >&2
  exit 1
fi

# bloqueia valores com quebra de linha (evita JS quebrado e surpresas)
validate_value() {
  v="$1"
  case "$v" in
    *"$(
      printf '\n'
    )"*|*"$(
      printf '\r'
    )"*)
      echo "[ERROR] Invalid value contains newline (blocked for safety)" >&2
      exit 1
      ;;
  esac
}

pairs=""
for key in $ALLOWED_VARS; do
  val="${!key:-}"
  [ -n "$val" ] || continue

  validate_value "$val"

  # escape básico para JS string
  esc_val=$(printf '%s' "$val" | sed 's/\\/\\\\/g; s/"/\\"/g')

  if [ -z "$pairs" ]; then
    pairs=$(printf '"%s":"%s"' "$key" "$esc_val")
  else
    pairs=$(printf '%s,"%s":"%s"' "$pairs" "$key" "$esc_val")
  fi
done

printf 'window.env={%s};\n' "$pairs" > "$OUT_FILE"
chmod 0444 "$OUT_FILE"

echo "[app-setup] Generated $OUT_FILE (allowlist + required OK)"
server {
    listen 8080;

    root /usr/share/nginx/html;
    index index.html;

    # env-config.js gerado em runtime
    location = /env-config.js {
        alias /tmp/runtime-config/env-config.js;
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
        add_header Content-Type "application/javascript; charset=utf-8" always;
    }

    # SPA fallback
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Headers básicos (ajuste conforme sua política)
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer" always;
    add_header X-Frame-Options "DENY" always;
}
services:
  web:
    image: sua-imagem:tag
    ports:
      - "8080:8080"

    environment:
      APP_BASE_URL: "https://frontend.exemplo.com"
      APP_API_URL: "https://api.exemplo.com"
      APP_ENV: "prod"
      APP_VERSION: "1.2.3"

    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev
      - /tmp/runtime-config:rw,noexec,nosuid,nodev
      - /var/cache/nginx:rw,noexec,nosuid,nodev
      - /var/run:rw,noexec,nosuid,nodev

    security_opt:
      - no-new-privileges:true

    cap_drop:
      - ALL

    # opcional: limita recursos (bom em auditoria)
    # deploy:
    #   resources:
    #     limits:
    #       cpus: "0.50"
    #       memory: "256M"
#!/bin/sh
set -eu

OUT_DIR="/tmp/runtime-config"
OUT_FILE="$OUT_DIR/runtime-config.json"

ALLOWED_VARS="
APP_BASE_URL
APP_API_URL
APP_AUTH_URL
APP_ENV
APP_VERSION
"

REQUIRED_VARS="
APP_BASE_URL
APP_API_URL
"

mkdir -p "$OUT_DIR"

missing=""
for var in $REQUIRED_VARS; do
  if [ -z "${!var:-}" ]; then
    missing="$missing $var"
  fi
done

if [ -n "$missing" ]; then
  echo "[ERROR] Missing required environment variables:$missing" >&2
  exit 1
fi

validate_value() {
  v="$1"
  case "$v" in
    *"$(
      printf '\n'
    )"*|*"$(
      printf '\r'
    )"*)
      echo "[ERROR] Invalid value contains newline (blocked for safety)" >&2
      exit 1
      ;;
  esac
}

json=""

for key in $ALLOWED_VARS; do
  val="${!key:-}"
  [ -n "$val" ] || continue

  validate_value "$val"

  # JSON string escape mínimo: \ e "
  esc_val=$(printf '%s' "$val" | sed 's/\\/\\\\/g; s/"/\\"/g')

  if [ -z "$json" ]; then
    json=$(printf '"%s":"%s"' "$key" "$esc_val")
  else
    json=$(printf '%s,"%s":"%s"' "$json" "$key" "$esc_val")
  fi
done

printf '{%s}\n' "$json" > "$OUT_FILE"
chmod 0444 "$OUT_FILE"

echo "[app-setup] Generated $OUT_FILE"
server {
    listen 8080;

    root /usr/share/nginx/html;
    index index.html;

    location = /runtime-config.json {
        alias /tmp/runtime-config/runtime-config.json;

        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;

        default_type application/json;
        add_header X-Content-Type-Options "nosniff" always;
    }

    location / {
        try_files $uri $uri/ /index.html;
    }

    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer" always;
    add_header X-Frame-Options "DENY" always;
}
async function loadRuntimeConfig() {
  const res = await fetch("/runtime-config.json", { cache: "no-store" });
  if (!res.ok) throw new Error("Failed to load runtime-config.json");
  return res.json();
}

loadRuntimeConfig()
  .then((cfg) => {
    (window as any).__RUNTIME_CONFIG__ = cfg;
    // render app
  })
  .catch((err) => {
    // opcional: render uma tela de erro “Config inválida”
    console.error(err);
  });
#!/bin/sh
set -eu

# Gera env-config.js em tmpfs (compatível com read-only rootfs)
# Serve via nginx (recomendado: location = /env-config.js { alias /tmp/runtime-config/env-config.js; ... })

OUT_DIR="/tmp/runtime-config"
OUT_FILE="$OUT_DIR/env-config.js"

# Variáveis obrigatórias (somente estas 2 entram no arquivo)
: "${APP_BASE_URL:?Missing required environment variable APP_BASE_URL}"
: "${APP_API_URL:?Missing required environment variable APP_API_URL}"

mkdir -p "$OUT_DIR"

# Normaliza CRLF (evita ^M do Windows)
APP_BASE_URL=$(printf '%s' "$APP_BASE_URL" | sed 's/\r$//')
APP_API_URL=$(printf '%s' "$APP_API_URL" | sed 's/\r$//')

# Bloqueia newline real por segurança (evita JS quebrado / comportamento inesperado)
case "$APP_BASE_URL" in
  *"$(
    printf '\n'
  )"*) echo "[ERROR] APP_BASE_URL contains newline (blocked for safety)" >&2; exit 1 ;;
esac
case "$APP_API_URL" in
  *"$(
    printf '\n'
  )"*) echo "[ERROR] APP_API_URL contains newline (blocked for safety)" >&2; exit 1 ;;
esac

# Escape mínimo para JS string
esc() {
  printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

BASE_ESC=$(esc "$APP_BASE_URL")
API_ESC=$(esc "$APP_API_URL")

cat <<EOF > "$OUT_FILE"
window.env={
  APP_BASE_URL: "$BASE_ESC",
  APP_API_URL: "$API_ESC"
};
EOF

chmod 0444 "$OUT_FILE"

echo "[app-setup] Generated $OUT_FILE"
read_only: true
tmpfs:
  - /tmp/runtime-config:rw,noexec,nosuid,nodev
  - /var/cache/nginx:rw,noexec,nosuid,nodev
  - /var/run:rw,noexec,nosuid,nodev
  - /tmp:rw,noexec,nosuid,nodev

Json

#!/bin/sh
set -eu

OUT_DIR="/tmp/runtime-config"
OUT_FILE="$OUT_DIR/runtime-config.json"

: "${APP_BASE_URL:?Missing required environment variable APP_BASE_URL}"
: "${APP_API_URL:?Missing required environment variable APP_API_URL}"

mkdir -p "$OUT_DIR"

# Normaliza CRLF no final (caso venha de Windows)
trim_eol() { printf '%s' "$1" | sed 's/[\r\n]*$//'; }

APP_BASE_URL="$(trim_eol "$APP_BASE_URL")"
APP_API_URL="$(trim_eol "$APP_API_URL")"

# Bloqueia newline dentro do valor (isso é config inválida)
case "$APP_BASE_URL" in *"$(
  printf '\n'
)"*|*"$(
  printf '\r'
)"*) echo "[ERROR] APP_BASE_URL contains newline (blocked)" >&2; exit 1 ;; esac

case "$APP_API_URL" in *"$(
  printf '\n'
)"*|*"$(
  printf '\r'
)"*) echo "[ERROR] APP_API_URL contains newline (blocked)" >&2; exit 1 ;; esac

# Escape mínimo para JSON string
json_escape() {
  printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

BASE_ESC="$(json_escape "$APP_BASE_URL")"
API_ESC="$(json_escape "$APP_API_URL")"

cat <<EOF > "$OUT_FILE"
{
  "APP_BASE_URL": "$BASE_ESC",
  "APP_API_URL": "$API_ESC"
}
EOF

chmod 0444 "$OUT_FILE"
echo "[runtime-config] Generated $OUT_FILE"
location = /runtime-config.json {
  alias /tmp/runtime-config/runtime-config.json;

  add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
  add_header Pragma "no-cache" always;
  add_header Expires "0" always;

  default_type application/json;
  add_header X-Content-Type-Options "nosniff" always;
}

Beleza. Vamos fechar isso de forma definitiva usando JSON (mais seguro e mais limpo que injetar JS), mantendo:

runtime (sem rebuild)

non-root

logs em arquivo no host (como você decidiu)

sem read_only (como você decidiu)

runtime-config.json gerado em /tmp/runtime-config

A solução fica estável e fácil de auditar.


  1. Script em /docker-entrypoint.d/90-runtime-config.sh

Gera somente 2 variáveis (rígido e simples), falha se faltar.

#!/bin/sh set -eu

OUT_DIR="/tmp/runtime-config" OUT_FILE="$OUT_DIR/runtime-config.json"

: "${APP_BASE_URL:?Missing required environment variable APP_BASE_URL}" : "${APP_API_URL:?Missing required environment variable APP_API_URL}"

mkdir -p "$OUT_DIR"

Normaliza CRLF no final (caso venha de Windows)

trim_eol() { printf '%s' "$1" | sed 's/[\r\n]*$//'; }

APP_BASE_URL="$(trim_eol "$APP_BASE_URL")" APP_API_URL="$(trim_eol "$APP_API_URL")"

Bloqueia newline dentro do valor (isso é config inválida)

case "$APP_BASE_URL" in "$( printf '\n' )"|"$( printf '\r' )") echo "[ERROR] APP_BASE_URL contains newline (blocked)" >&2; exit 1 ;; esac

case "$APP_API_URL" in "$( printf '\n' )"|"$( printf '\r' )") echo "[ERROR] APP_API_URL contains newline (blocked)" >&2; exit 1 ;; esac

Escape mínimo para JSON string

json_escape() { printf '%s' "$1" | sed 's/\/\\/g; s/"/\"/g' }

BASE_ESC="$(json_escape "$APP_BASE_URL")" API_ESC="$(json_escape "$APP_API_URL")"

cat < "$OUT_FILE" { "APP_BASE_URL": "$BASE_ESC", "APP_API_URL": "$API_ESC" } EOF

chmod 0444 "$OUT_FILE" echo "[runtime-config] Generated $OUT_FILE"

Coloque esse arquivo no seu repo e copie no Dockerfile para /docker-entrypoint.d/90-runtime-config.sh.


  1. Nginx: servir o JSON (com no-cache) via alias

No seu server { ... }:

location = /runtime-config.json { alias /tmp/runtime-config/runtime-config.json;

add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always; add_header Pragma "no-cache" always; add_header Expires "0" always;

default_type application/json; add_header X-Content-Type-Options "nosniff" always; }

E o resto da SPA:

location / { try_files $uri $uri/ /index.html; }


  1. Dockerfile: copiar script e manter non-root + logs

Exemplo (ajuste paths conforme seu repo):

FROM nginx:1.29.4-alpine

nginx conf

COPY --chown=nginx:nginx nginx/nginx.conf /etc/nginx/nginx.conf COPY --chown=nginx:nginx nginx/default.conf /etc/nginx/conf.d/default.conf

entrypoint script

COPY --chown=nginx:nginx --chmod=0755 nginx/90-runtime-config.sh /docker-entrypoint.d/90-runtime-config.sh

build estático

COPY --chown=nginx:nginx build/ /usr/share/nginx/html/

garante diretórios de log/cache (porque você vai mapear logs no host)

RUN mkdir -p /var/cache/nginx /var/log/nginx /tmp/runtime-config
&& chown -R nginx:nginx /var/cache/nginx /var/log/nginx /tmp

USER nginx EXPOSE 8080


  1. docker-compose.yml: env + logs no host

services: web: image: sua-imagem container_name: servcelweb-vivo-pre-front restart: always

ports:
  - "8080:8080"

environment:
  APP_BASE_URL: "http://localhost:8080"
  APP_API_URL: "http://selcdspgw002.paulista.local:8083/servcel/api/v1"

volumes:
  - ./logs/nginx:/var/log/nginx

Atenção: crie a pasta antes:

mkdir -p ./logs/nginx

Se der permissão, aplique (rápido):

chmod 777 ./logs/nginx

Ou (mais correto), alinhe UID/GID do nginx e chown no host.


  1. React: carregar JSON antes de renderizar (definitivo)

src/runtime-config.ts

export type RuntimeConfig = { APP_BASE_URL: string; APP_API_URL: string; };

declare global { interface Window { RUNTIME_CONFIG?: RuntimeConfig; } }

export async function loadRuntimeConfig(): Promise { const res = await fetch("/runtime-config.json", { cache: "no-store" }); if (!res.ok) { throw new Error(Failed to load runtime-config.json (${res.status})); } return (await res.json()) as RuntimeConfig; }

src/main.tsx (ou index.tsx)

Carregue antes do createRoot().render(...):

import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { loadRuntimeConfig } from "./runtime-config";

async function bootstrap() { const cfg = await loadRuntimeConfig(); window.RUNTIME_CONFIG = cfg;

ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> </React.StrictMode> ); }

bootstrap().catch((err) => { // aqui você pode renderizar uma tela simples de erro de config console.error(err); });

Uso no app

const apiUrl = window.RUNTIME_CONFIG?.APP_API_URL;


  1. Teste final (sem dúvida)

Suba:

docker compose down --remove-orphans docker compose up --build

Teste:

curl -i http://localhost:8080/runtime-config.json

Você deve ver:

200 OK

Cache-Control: no-store...

JSON com as duas chaves


Por que isso resolve “de forma definitiva”

JSON não executa código.

Você não depende de <script src="...">.

O arquivo é gerado no start (runtime).

O Nginx serve via alias direto do /tmp.

O React carrega uma vez e pronto.

Se você quiser, eu também ajusto seu nginx.conf completo para:

manter logs em arquivo (host)

e evitar qualquer conflito com try_files que faça o JSON cair no index.html.


SCRIPT_PATH="$($READLINK -f "$0" 2>/dev/null || python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$0")"
ROOT="$(dirname "$SCRIPT_PATH")"
SCRIPT_PATH="$($READLINK -f "$0")"
ROOT="$(dirname "$SCRIPT_PATH")"

git submodule

  • 4c28c9621d6d2f60638bf802deba3fb6

gerenciamento memoria em C

  • 30902f0a293ef58f4017d882220a72b6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment