- Documentação
- Deploys
- Sites Estáticos & nginx
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 (
8080recomendado) 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 logsconsiga lê-los. - Serviço criado com
--publicpara o site ser acessível pela internet. - É uma SPA? Um fallback
try_files … /index.htmlestá configurado para o roteamento no cliente.