Nesta página

Sites Estáticos & nginx

Servir um front-end já buildado (React, Vue, Angular, Astro, HTML puro) no Guara Cloud funciona muito bem — mas a imagem nginx padrão não roda do jeito que vem. Esta página explica o porquê e traz receitas prontas para copiar e colar que funcionam de primeira.

Resumindo: os containers no Guara Cloud rodam como um usuário non-root (UID 1000) e ouvem em uma porta não privilegiada. Qualquer servidor web que você usar precisa respeitar essas duas regras. Use nginxinc/nginx-unprivileged (Receita 1) e você resolve em um minuto.

Por que a imagem nginx padrão não funciona

A imagem oficial nginx foi feita para rodar como root: ela faz bind na porta 80 e pré-cria e escreve em /var/cache/nginx, /var/log/nginx e /var/run/nginx.pid. No Guara Cloud o container é forçado para o UID 1000 e recebe apenas a capability restrita NET_BIND_SERVICE; uma porta baixa configurada pode ser usada, mas as suposições da imagem sobre caminhos pertencentes ao root continuam invalidas. A plataforma tambem monta volumes graváveis pelo grupo em /var/cache, /var/log, /var/run e /tmp, que sobrepõem o que a imagem já trazia pronto nesses caminhos. O comportamento na inicialização varia conforme a versão da imagem, mas você pode ver erros como o processo master falhando ao abrir o arquivo de log, já que /var/log/nginx deixa de existir dentro do volume vazio que sobrepôs o original:

nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (2: No such file or directory)

Você também pode ver o aviso inofensivo mas barulhento:

nginx: [warn] the "user" directive makes sense only if the master process runs with super-user privileges, ignored

O Guara Cloud monta volumes efêmeros graváveis em /tmp, /var/run, /var/cache e /var/log, mas esses mounts sobrepõem a árvore de diretórios que a imagem padrão traz, e o nginx.conf padrão continua apontando para caminhos e uma porta que assumem root. A solução é rodar uma imagem (ou config) que espere um usuário non-root — exatamente o que as receitas abaixo fazem.

Receita 1 (recomendada): nginx-unprivileged

nginxinc/nginx-unprivileged é a imagem oficial do nginx, pré-configurada para rodar como usuário non-root, ouvir na porta 8080 e logar em stdout/stderr. Não precisa de config customizada.

FROM nginxinc/nginx-unprivileged:alpine

# Copie a saída do build (Vite/CRA: dist ou build; Astro: dist)
COPY dist/ /usr/share/nginx/html

Crie o serviço na porta 8080 e deixe-o público:

guara services create --port 8080 --public

Pronto. A imagem já ouve na 8080, escreve arquivos temporários em /tmp e envia os logs para stdout/stderr, onde o guara logs consegue lê-los.

Receita 2: nginx padrão com config customizada

Se você precisar usar a imagem nginx padrão, sobrescreva o nginx.conf para que todo caminho gravável fique dentro de /tmp e o servidor ouça na 8080.

nginx.conf:

pid /tmp/nginx.pid;

events {}

http {
  # Redireciona os caminhos temporários do nginx para um local onde o UID 1000 pode escrever.
  client_body_temp_path /tmp/client_temp;
  proxy_temp_path       /tmp/proxy_temp;
  fastcgi_temp_path     /tmp/fastcgi_temp;
  uwsgi_temp_path       /tmp/uwsgi_temp;
  scgi_temp_path        /tmp/scgi_temp;

  # Loga no stdout/stderr do container, não em arquivos.
  access_log /dev/stdout;
  error_log  /dev/stderr;

  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;
  sendfile      on;

  server {
    listen 8080;
    root   /usr/share/nginx/html;
    index  index.html;

    # Fallback de SPA: serve index.html para rotas do cliente.
    location / {
      try_files $uri $uri/ /index.html;
    }
  }
}

Dockerfile:

FROM nginx:alpine

# Substitui a config padrão orientada a root.
COPY nginx.conf /etc/nginx/nginx.conf
COPY dist/ /usr/share/nginx/html
guara services create --port 8080 --public

Repare que não há diretiva user no topo: sem ela, o nginx roda os workers como o usuário atual (UID 1000) e pula o aviso de queda de privilégio.

Receita 3: alternativas ao nginx

Qualquer servidor que ouça em uma porta não privilegiada como usuário non-root funciona. Algumas opções leves:

Caddy

O Caddy serve arquivos estáticos sem exigir root. Configure a porta de escuta explicitamente; mudar a porta alvo do servico Guara nao muda a porta interna do Caddy.

Adicione um Caddyfile:

:8080 {
  root * /usr/share/caddy
  encode gzip
  try_files {path} /index.html
  file_server
}
FROM caddy:alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY dist/ /usr/share/caddy
guara services create --port 8080 --public

BusyBox httpd

Minúsculo (~5 MB) e amigável a non-root:

FROM busybox:musl
COPY dist/ /var/www
EXPOSE 8080
CMD ["httpd", "-f", "-v", "-p", "8080", "-h", "/var/www"]
guara services create --port 8080 --public

Servidores Node estáticos (serve, http-server)

Úteis se o seu build já produz uma imagem Node:

FROM node:20-alpine
WORKDIR /app
RUN npm install -g serve
COPY dist/ ./dist
EXPOSE 8080
# -s ativa o fallback de SPA para index.html
CMD ["serve", "-s", "dist", "-l", "8080"]
guara services create --port 8080 --public

Checklist

  • O servidor ouve em uma porta não privilegiada (8080 recomendado) e ela bate com --port.
  • Nenhuma dependência de ser root — prefira uma imagem feita para non-root, ou redirecione escritas para /tmp.
  • Os logs vão para stdout/stderr para que o guara logs consiga lê-los.
  • Serviço criado com --public para o site ser acessível pela internet.
  • É uma SPA? Um fallback try_files … /index.html está configurado para o roteamento no cliente.