Operations
Configuração Nginx
Reverse proxy com TLS, sub-domínios por serviço, headers de segurança.
Nginx é o único entrypoint público em prod. Termina TLS, faz roteamento por sub-domínio, aplica rate limit básico, e adiciona headers de segurança.
Estrutura
/apps/geek-social/nginx/
├── geek-social.conf ← config principal
├── ssl-options.conf ← settings SSL/TLS
└── proxy-params.conf ← headers de proxy comunsssl-options.conf
# Modern SSL config (Mozilla intermediate)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;proxy-params.conf
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 60s;
proxy_send_timeout 60s;E no topo do geek-social.conf (uma vez no http block):
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}geek-social.conf
# ===== Rate limit zones =====
limit_req_zone $binary_remote_addr zone=api_general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=api_auth:10m rate=5r/m;
# ===== Upstreams =====
upstream geek_api {
server geek-social-api:3003;
keepalive 32;
}
upstream geek_frontend {
server geek-social-frontend:8080;
}
upstream geek_docs {
server geek-social-docs:3030;
}
upstream geek_minio {
server geek-social-minio:9000;
}
# ===== Redirects HTTP → HTTPS =====
server {
listen 80 default_server;
server_name _;
# Certbot challenge
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# ===== Frontend (app.geek-social.com.br) =====
server {
listen 443 ssl http2;
server_name app.geek-social.com.br;
ssl_certificate /etc/letsencrypt/live/geek-social.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/geek-social.com.br/privkey.pem;
include /etc/nginx/conf.d/ssl-options.conf;
# Headers de segurança
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(self), camera=(self)" always;
# Compressão
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/wasm image/svg+xml;
gzip_min_length 256;
# Cache de assets (build versionado por hash)
location ~* \.(js|css|woff2|webp|png|jpg|svg)$ {
proxy_pass http://geek_frontend;
include /etc/nginx/conf.d/proxy-params.conf;
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback
location / {
proxy_pass http://geek_frontend;
include /etc/nginx/conf.d/proxy-params.conf;
}
}
# ===== API (api.geek-social.com.br) =====
server {
listen 443 ssl http2;
server_name api.geek-social.com.br;
ssl_certificate /etc/letsencrypt/live/geek-social.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/geek-social.com.br/privkey.pem;
include /etc/nginx/conf.d/ssl-options.conf;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
# CORS é tratado pelo Fastify backend (NÃO duplicar aqui)
# Rate limit em endpoints sensíveis
location ~ ^/auth/(login|register|forgot-password) {
limit_req zone=api_auth burst=10 nodelay;
proxy_pass http://geek_api;
include /etc/nginx/conf.d/proxy-params.conf;
}
# Rate limit geral
limit_req zone=api_general burst=50 nodelay;
# Socket.IO (precisa Upgrade headers)
location /socket.io/ {
proxy_pass http://geek_api;
include /etc/nginx/conf.d/proxy-params.conf;
proxy_read_timeout 7d; # WebSocket pode ficar idle
}
# Multipart upload pode ser maior
location ~ ^/(users/me/(avatar|cover|background)|posts/.*/media|chat/.*/attachments) {
client_max_body_size 50m;
proxy_pass http://geek_api;
include /etc/nginx/conf.d/proxy-params.conf;
}
# Default
location / {
client_max_body_size 5m;
proxy_pass http://geek_api;
include /etc/nginx/conf.d/proxy-params.conf;
}
}
# ===== Docs (docs.geek-social.com.br) =====
server {
listen 443 ssl http2;
server_name docs.geek-social.com.br;
ssl_certificate /etc/letsencrypt/live/geek-social.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/geek-social.com.br/privkey.pem;
include /etc/nginx/conf.d/ssl-options.conf;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
gzip on;
gzip_types text/html text/css application/javascript application/json image/svg+xml;
location / {
proxy_pass http://geek_docs;
include /etc/nginx/conf.d/proxy-params.conf;
}
# Cache estáticos
location ~* \.(js|css|woff2|png|jpg|svg)$ {
proxy_pass http://geek_docs;
include /etc/nginx/conf.d/proxy-params.conf;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# ===== CDN MinIO (cdn.geek-social.com.br) =====
server {
listen 443 ssl http2;
server_name cdn.geek-social.com.br;
ssl_certificate /etc/letsencrypt/live/geek-social.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/geek-social.com.br/privkey.pem;
include /etc/nginx/conf.d/ssl-options.conf;
# Não força HTTPS em assets estáticos (pra browsers antigos)
add_header Cache-Control "public, max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
client_max_body_size 0; # Sem limit (uploads vão pelo backend, não direto aqui)
location / {
proxy_pass http://geek_minio;
include /etc/nginx/conf.d/proxy-params.conf;
}
}
# ===== Apex redirect (geek-social.com.br → app) =====
server {
listen 443 ssl http2;
server_name geek-social.com.br;
ssl_certificate /etc/letsencrypt/live/geek-social.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/geek-social.com.br/privkey.pem;
include /etc/nginx/conf.d/ssl-options.conf;
return 301 https://app.geek-social.com.br$request_uri;
}Frontend Dockerfile (Nginx interno)
geek-social-frontend/Dockerfile:
# Build stage
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Serve stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY entrypoint.sh /docker-entrypoint.d/30-runtime-config.sh
RUN chmod +x /docker-entrypoint.d/30-runtime-config.sh
EXPOSE 8080geek-social-frontend/nginx.conf (interno do container):
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
# SW e manifest sem cache
location ~ \.(webmanifest|sw\.js)$ {
add_header Cache-Control "no-cache";
}
}entrypoint.sh injeta config runtime:
#!/bin/sh
cat > /usr/share/nginx/html/config.js <<EOF
window.APP_CONFIG = {
apiUrl: '${API_URL:-http://localhost:3003}',
vapidPublicKey: '${VAPID_PUBLIC_KEY:-}',
};
EOFDocs Dockerfile
geek-social-docs/Dockerfile:
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3030
CMD ["node", "server.js"](Requires output: 'standalone' no next.config.mjs.)
API Dockerfile
geek-social-api/Dockerfile:
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN apk add --no-cache curl # pra healthcheck
EXPOSE 3003
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -fsS http://localhost:3003/health || exit 1
CMD ["node", "dist/server.js"]Validar config Nginx
docker exec geek-social-nginx nginx -tAntes de aplicar mudanças, sempre valide.
Reload sem downtime
docker exec geek-social-nginx nginx -s reloadOu:
docker compose kill -s HUP nginxSubstitui workers gracefully.
Logs
# Access log
docker exec geek-social-nginx tail -f /var/log/nginx/access.log
# Error log
docker exec geek-social-nginx tail -f /var/log/nginx/error.logEm prod, mount esses pra fora pra log shipping (Promtail → Loki).
Hardening adicional
# Hide Nginx version
server_tokens off;
# Limit request size globalmente (override por location)
client_max_body_size 5m;
# Slow read attack
client_body_timeout 10s;
client_header_timeout 10s;
# Slow loris
keepalive_timeout 5s 5s;
# Buffer overflow
client_body_buffer_size 1k;
client_header_buffer_size 1k;
large_client_header_buffers 2 1k;Checklist Nginx
- SSL config moderno (Mozilla intermediate)
- HSTS header em todos servers
- Rate limit em /auth/* sensíveis
- Compressão gzip
- Cache headers em estáticos
- WebSocket upgrade configurado
- Multipart limit ajustado por path
- Health check da config (
nginx -t) - Logs centralizados
- Server tokens off