Geek Social — Documentação
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 comuns

ssl-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 8080

geek-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:-}',
};
EOF

Docs 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 -t

Antes de aplicar mudanças, sempre valide.

Reload sem downtime

docker exec geek-social-nginx nginx -s reload

Ou:

docker compose kill -s HUP nginx

Substitui 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.log

Em 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

Referências

On this page