witdev-project-setup
WitDev Project Setup — Skill
Guia canônico para criar e configurar novos projetos com a stack WitDev padrão. Garante consistência entre todos os projetos em termos de versões, scripts, estrutura Docker e fluxo de deploy.
Stack Pinada (Março 2026)
| Tecnologia | Versão / Imagem | Justificativa |
|---|---|---|
| PostgreSQL + pgvector | pgvector/pgvector:pg17 |
pgvector 0.8.x já incluso |
| Redis | redis:8-alpine |
Major pin, patches automáticos, ~15MB |
| Node.js | node:24-alpine |
LTS "Krypton", suporte até Abr/2028 |
| Next.js | next@^16.1.x |
Turbopack estável, React 19.2 |
| Prisma | latest compatible | ORM padrão — migrate deploy em prod |
| BullMQ | latest | Filas Redis |
| NextAuth.js | v5 + Prisma adapter | Auth padrão |
Regra de versão: nunca use
latestpara banco/cache (pode quebrar com major). Useredis:8-alpine, nãoredis:latestnemredis:8.6.1.
Estrutura de Diretórios
meu-projeto/
├── app/ # Next.js App Router
│ ├── api/ # API Routes
│ └── (pages)/
├── lib/ # Utilities core (db, auth, redis, etc.)
├── components/ # React components
├── services/ # Business logic
├── worker/ # Background jobs (BullMQ)
├── prisma/
│ ├── schema.prisma
│ └── migrations/ # SEMPRE commitado com o schema
├── scripts/
│ └── db-prepare.js # Bootstrap DB no startup
├── docker-compose-dev.yml
├── docker-compose-producao.yaml
├── Dockerfile.prod
├── dev.sh # Gerenciador do ambiente de dev
├── build.sh # Build + Push + Deploy prod
├── .env.example
├── .env.development # Ignorado pelo git
├── .env.local # Ignorado pelo git (contém Portainer, etc.)
├── biome.json # Lint/format
├── package.json
├── tsconfig.json
└── CLAUDE.md / AGENTS.md # Contexto para AI agents
Workflow: Criar Novo Projeto
Passo 1 — Scaffolding Next.js
npx create-next-app@latest meu-projeto \
--typescript --tailwind --eslint --app --src-dir --no --import-alias "@/*"
cd meu-projeto
Passo 2 — Dependências
pnpm add @prisma/client bullmq redis next-auth@5 @auth/prisma-adapter \
@t3-oss/env-nextjs zod sonner
pnpm add -D prisma @biomejs/biome typescript @types/node @types/react
Passo 3 — Docker Compose Dev
# docker-compose-dev.yml
services:
postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_USER: app_user
POSTGRES_PASSWORD: app_password_dev
POSTGRES_DB: app_db
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:8-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
app:
build:
context: .
dockerfile: Dockerfile.dev
working_dir: /app
volumes:
- .:/app
- node_modules:/app/node_modules
ports:
- "3000:3000"
env_file:
- .env.development
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
worker:
build:
context: .
dockerfile: Dockerfile.dev
working_dir: /app
volumes:
- .:/app
- node_modules:/app/node_modules
env_file:
- .env.development
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
command: pnpm run worker
volumes:
node_modules:
postgres_data:
redis_data:
Nota volumes:
node_modulesé volume Docker isolado do host. Postgres e Redis são persistentes. Não crie volume separado para.next.
Passo 4 — Script scripts/db-prepare.js
Script Node.js executado no startup (dentro e fora do container) que:
- Aguarda o Postgres estar disponível (retry loop)
- Cria o database se não existir
- Habilita a extensão
vector(pgvector) - Roda
prisma migrate deployouprisma db pushconforme o modo - (Opcional) Roda seed
#!/usr/bin/env node
"use strict";
// Carrega .env conforme NODE_ENV
const fs = require("fs"), path = require("path"), { execSync } = require("child_process");
const envFiles = process.env.NODE_ENV === "production"
? [".env.production", ".env"]
: [".env.development", ".env.local", ".env"];
for (const f of envFiles) {
const p = path.join(process.cwd(), f);
if (fs.existsSync(p)) { require("dotenv").config({ path: p }); break; }
}
const { PrismaClient } = require("@prisma/client");
const MODE = process.argv.includes("--mode=deploy") ? "deploy" : "reset";
const RETRIES = 60, SLEEP_MS = 2000;
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function waitDB(prisma) {
for (let i = 1; i <= RETRIES; i++) {
try { await prisma.$connect(); await prisma.$disconnect(); return; }
catch (e) { console.log(`⏳ Aguardando Postgres (${i}/${RETRIES})...`); await sleep(SLEEP_MS); }
}
throw new Error("Postgres não disponível após timeout");
}
async function main() {
const prisma = new PrismaClient();
await waitDB(prisma);
// Habilitar pgvector
await prisma.$executeRawUnsafe("CREATE EXTENSION IF NOT EXISTS vector");
console.log("✅ pgvector habilitado");
// Aplicar migrations
const prismaBin = fs.existsSync("node_modules/.bin/prisma") ? "node_modules/.bin/prisma" : "prisma";
execSync(`${prismaBin} migrate deploy`, { stdio: "inherit" });
console.log("✅ Migrations aplicadas");
await prisma.$disconnect();
}
main().catch(e => { console.error("❌ db-prepare falhou:", e); process.exit(1); });
Variáveis de ambiente do db-prepare:
| Variável | Default | Descrição |
|---|---|---|
DB_CONNECT_RETRIES |
60 |
Tentativas de conexão |
DB_CONNECT_SLEEP_MS |
2000 |
Intervalo entre tentativas (ms) |
PRISMA_RUN_SEED |
false |
Rodar seed após migration |
VECTOR_REQUIRED |
true |
Falhar se pgvector não disponível |
EMBEDDING_DIMS |
1536 |
Dimensões do vetor (OpenAI = 1536) |
Passo 5 — dev.sh
Script bash que gerencia o ciclo completo de dev. Padrão obrigatório:
#!/usr/bin/env bash
# dev.sh - Gerenciador do ambiente de desenvolvimento
# Uso: ./dev.sh → sobe tudo
# ./dev.sh -n → sobe tudo + ngrok
# ./dev.sh build → rebuild incremental
# ./dev.sh build --no-cache → rebuild limpo
# ./dev.sh down → para containers
# ./dev.sh logs [service] → ver logs
# ./dev.sh shell → abre shell no container app
# ./dev.sh prisma → Prisma Studio
set -euo pipefail
Funções obrigatórias no dev.sh:
ensure_env_file— copia.env.example→.env.developmentse não existirensure_network— cria rede Docker externa (minha_rede) se não existirdc()— wrapper Docker Compose (suporta v1 e v2)cmd_up— sobe + tail logs + trap Ctrl+C para graceful stopcmd_build— para containers → remove volumenode_modules→ rebuild → sobe → gera Prisma Client → roda migrationscmd_logs [service] [tar|cat]— logs com opções de exportar/compactarprint_urls— exibe URLs do app, infraestrutura e comandos úteis
Cores padronizadas:
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
log_info() { echo -e "${BLUE}ℹ${NC} $1"; }
log_success() { echo -e "${GREEN}✔${NC} $1"; }
log_warn() { echo -e "${YELLOW}⚠${NC} $1"; }
log_error() { echo -e "${RED}✖${NC} $1"; }
log_header() { echo -e "\n${BOLD}${CYAN}═══ $1 ═══${NC}\n"; }
Passo 6 — build.sh (Produção)
Script de build + push Docker Hub + deploy automático via Portainer:
# Uso:
./build.sh # build, push latest, force-update prod
./build.sh v1.2.3 # build, push e tag v1.2.3
./build.sh --no-deploy # só build + push, sem atualizar prod
Fluxo obrigatório:
- Verificar e iniciar Postgres local para build SSG (Next.js precisa do DB no build)
docker compose build app- Tag da imagem
docker push(latest + versão se diferente)- Sleep 5s para propagação no registry
- Force-update de serviços Swarm via Portainer Docker Proxy API
Ordem de update dos serviços: Worker ANTES da App (evita race condition BullMQ).
Portainer config em .env.local:
PORTAINER_URL=https://portainer.witdev.com.br
PORTAINER_API_KEY=ptr_xxxxxxxxxxxx
PORTAINER_ENDPOINT_ID=1
Banco de Dados — Regras Obrigatórias
# ✅ DEV: criar migration
pnpm exec prisma migrate dev --name descricao
# ✅ PROD: aplicar migrations pendentes
pnpm exec prisma migrate deploy
# ❌ NUNCA usar (destroem histórico e causam drift):
# prisma db pull
# prisma db push
Drift em DEV (aceitável resetar):
pnpm exec prisma migrate reset --force # DEV ONLY
pnpm exec prisma migrate dev --name fix
A pasta
prisma/migrations/SEMPRE deve ser commitada junto comschema.prisma.
Lib Redis Singleton (lib/redis.ts)
import { createClient, type RedisClientType } from "redis";
const globalForRedis = globalThis as unknown as { redis?: RedisClientType };
function getRedisClient(): RedisClientType {
if (!globalForRedis.redis) {
const client = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
client.on("error", (err) => console.error("Redis error:", err));
client.connect();
globalForRedis.redis = client as RedisClientType;
}
return globalForRedis.redis;
}
export const redis = getRedisClient();
Dockerfile.prod (Padrão Multi-Stage)
# ── Build ──────────────────────────────────────────────────────────────────
FROM node:24-alpine AS builder
WORKDIR /app
# Install deps separado para aproveitar cache
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
# Precisa do DB disponível para prisma generate + SSG
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
RUN pnpm exec prisma generate
RUN pnpm run build
# ── Runtime ────────────────────────────────────────────────────────────────
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
COPY /app/public ./public
COPY /app/scripts ./scripts
COPY /app/prisma ./prisma
COPY /app/node_modules/.prisma ./node_modules/.prisma
COPY /app/node_modules/@prisma ./node_modules/@prisma
EXPOSE 3000
CMD ["sh", "-c", "node scripts/db-prepare.js --mode=deploy && node server.js"]
.env.example Mínimo
# Database
DATABASE_URL=postgresql://app_user:app_password_dev@localhost:5432/app_db
# Redis
REDIS_URL=redis://localhost:6379
# Auth
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=change-me-in-production
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Portainer (só em .env.local, não commitado)
# PORTAINER_URL=https://portainer.witdev.com.br
# PORTAINER_API_KEY=ptr_xxxxxxxxxxxx
# PORTAINER_ENDPOINT_ID=1
package.json — Scripts Padrão
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "biome check .",
"lint-apply": "biome check --write .",
"format-apply": "biome format --write .",
"worker": "tsx watch worker/index.ts",
"start:worker": "node dist/worker/index.js",
"db:prepare": "node scripts/db-prepare.js",
"db:generate": "prisma generate",
"db:studio": "prisma studio",
"db:migrate": "prisma migrate dev",
"db:seed": "prisma db seed",
"type-check": "tsc --noEmit && tsc --noEmit -p tsconfig.worker.json"
}
}
Checklist de Novo Projeto
□ npx create-next-app@latest com --typescript --tailwind --app
□ Instalar dependências base (pnpm add ...)
□ docker-compose-dev.yml com pgvector:pg17 + redis:8-alpine + healthchecks
□ .env.example com DATABASE_URL, REDIS_URL, NEXTAUTH_SECRET
□ scripts/db-prepare.js com retry + ensure extension + migrate deploy
□ lib/db.ts (pool pg singleton) ou Prisma singleton
□ lib/redis.ts (redis client singleton)
□ dev.sh com todos os comandos padronizados (up/build/down/logs/shell/prisma)
□ build.sh com multi-step: postgres start → build → push → Portainer deploy
□ Dockerfile.prod multi-stage node:24-alpine
□ biome.json configurado
□ CLAUDE.md / AGENTS.md com contexto do projeto
□ prisma/schema.prisma com provider = "postgresql" + previewFeatures = ["postgresqlExtensions"]
□ Testar: ./dev.sh → app em localhost:3000 sem erros
□ Verificar pgvector: SELECT * FROM pg_extension WHERE extname = 'vector';
□ Verificar Redis: docker exec <container> redis-cli ping → PONG
□ type-check: pnpm exec tsc --noEmit
Comandos Úteis (Referência Rápida)
# Dev
./dev.sh # sobe tudo
./dev.sh -n # sobe tudo + ngrok
./dev.sh build # rebuild incremental ⚡
./dev.sh build --no-cache # rebuild limpo 🐌
./dev.sh logs # todos os logs
./dev.sh logs worker # logs do worker (tempo real)
./dev.sh logs worker tar # compactar logs do worker
./dev.sh shell # shell no container app
./dev.sh prisma # Prisma Studio
# DB
pnpm exec prisma migrate dev --name X # ✅ criar migration (dev)
pnpm exec prisma migrate deploy # ✅ aplicar em prod
pnpm exec prisma generate # regenerar client
pnpm exec prisma studio # GUI do banco
# Produção
./build.sh # build + push + deploy
./build.sh v1.2.3 # build + push tag + deploy
./build.sh --no-deploy # só build + push
# TypeScript
pnpm exec tsc --noEmit && pnpm exec tsc --noEmit -p tsconfig.worker.json
# Docker Prod (SSH)
docker service ls
docker service logs socialwise_app --tail 200
docker service logs socialwise_worker --tail 200
Conexões em Docker — Evitando "Conexão Fantasma"
O Problema
Em Docker/Swarm, a tabela NAT do bridge network (nf_conntrack) descarta conexões TCP idle silenciosamente (~30min). O app acha que a conexão com Postgres ainda existe, manda uma query, e recebe timeout → conexão fantasma.
O que NÃO funciona
TCP keepalive na connection string do Prisma:
DATABASE_URL=postgresql://...?tcp_keepalive=1&tcp_keepalive_idle=120&tcp_keepalive_interval=10
O Prisma usa um query engine em Rust, não o driver pg do Node.js. Parâmetros tcp_keepalive_idle e tcp_keepalive_interval são ignorados silenciosamente. Prisma só entende connect_timeout, pool_timeout e socket_timeout na URL.
A Solução Correta (por camada)
Cada driver tem capacidades diferentes — a solução depende de qual você usa:
| Driver | TCP keepalive nativo? | Solução |
|---|---|---|
| pg.Pool (Node.js) | Sim — keepAlive: true |
2 linhas, zero overhead, o OS cuida |
| ioredis | Sim — keepAlive: 10000 |
1 linha |
Prisma + Driver Adapter (@prisma/adapter-pg) |
Sim — usa pg.Pool por baixo | Adapter + keepAlive: true, sem heartbeat |
| Prisma engine Rust (padrão) | Não — sem config exposta | Heartbeat SELECT 1 a cada 4min (workaround) |
1. pg Pool direto — TCP keepalive no nível do socket (SOLUÇÃO IDEAL):
import pg from "pg";
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
keepAlive: true, // ← TCP keepalive ativa
keepAliveInitialDelayMillis: 10000, // ← primeiro keepalive após 10s idle
});
O pg do Node configura o socket TCP diretamente. O OS envia pacotes periódicos que mantêm a entrada na tabela NAT e detectam conexões mortas. Sem queries fake, sem overhead, sem intervalo.
2. Prisma — Duas opções:
O Prisma query engine padrão é escrito em Rust e gerencia seu próprio pool de conexões. Não existe config para TCP keepalive nos sockets que ele abre. Sem o Rust engine, o Prisma fica refém das configs globais do OS do container.
Opção A — Driver Adapter @prisma/adapter-pg (solução ideal, Prisma 5+):
Usa o driver pg do Node.js em vez do engine Rust. Com isso, keepAlive: true funciona nativamente — sem heartbeat.
pnpm add @prisma/adapter-pg pg
// schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
// lib/prisma.ts — Com Driver Adapter (sem heartbeat!)
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import pg from "pg";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
if (!globalForPrisma.prisma) {
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
keepAlive: true, // ← TCP keepalive nativo!
keepAliveInitialDelayMillis: 10000,
});
const adapter = new PrismaPg(pool);
globalForPrisma.prisma = new PrismaClient({ adapter });
}
export const prisma = globalForPrisma.prisma;
Opção B — Engine Rust padrão + Heartbeat (workaround, se não puder usar adapter):
// lib/prisma.ts — Singleton com heartbeat
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === "production" ? ["error"] : ["error", "warn"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
// Heartbeat — mantém conexão viva na tabela NAT do Docker
// Necessário porque o engine Rust do Prisma não expõe TCP keepalive
const HEARTBEAT_MS = 4 * 60 * 1000; // 4 min (< nf_conntrack timeout ~30min)
setInterval(async () => {
try { await prisma.$queryRaw`SELECT 1`; }
catch { /* Prisma 5+ reconecta automaticamente no próximo uso */ }
}, HEARTBEAT_MS);
Preferência: Opção A > Opção B. O Driver Adapter elimina o heartbeat, usa TCP keepalive real do OS, e reduz overhead (sem queries fake a cada 4min). A Opção B é aceitável para projetos existentes onde migrar para adapter é arriscado.
Connection string — somente params que o Prisma REALMENTE suporta:
DATABASE_URL=postgresql://user:pass@host:5432/db?connect_timeout=10&pool_timeout=30
⚠️ NÃO use
socket_timeoutem produção — ele mata QUALQUER query que exceda o tempo (incluindo migrations, relatórios, bulk inserts). É como "matar o paciente para curar a febre".⚠️
tcp_keepalive*na URL é IGNORADO pelo Prisma padrão (engine Rust) — ele não passa esses parâmetros pro socket. Funciona com pg.Pool e com Driver Adapter, não com o engine Rust.⚠️
relationMode = "prisma"NÃO tem relação com conexão/keepalive — serve para emular Foreign Keys via software (PlanetScale/Vitess). Não altera nada na camada de rede TCP.
3. Redis (ioredis) — keepalive nativo + retry:
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL, {
keepAlive: 10000, // TCP keepalive a cada 10s
maxRetriesPerRequest: null, // obrigatório para BullMQ
retryStrategy: (times) => Math.min(times * 100, 3000),
enableReadyCheck: true,
lazyConnect: true,
});
Anti-padrão: NÃO faça isso
withPrismaReconnect()com locks e retry manual — Prisma 5+ reconecta automaticamente no pool. O heartbeat simples é suficiente, o wrapper de 100+ linhas duplica lógica interna.- Lock
__prismaConnectLock—$connect()já é serializado internamente pelo Prisma. - Mock Redis inline no código de produção — separe em arquivo de teste.
- 20+ console.log com emojis — poluem stdout em prod.
Resumo:
- pg.Pool direto:
keepAlive: true(solução ideal, 2 linhas) - Prisma + Driver Adapter (
@prisma/adapter-pg): pg.Pool comkeepAlive: truepor baixo (solução ideal para Prisma, sem heartbeat) - Prisma engine Rust (padrão): Heartbeat
SELECT 1a cada 4min (workaround aceitável, 5 linhas) - ioredis:
keepAlive: 10000(nativo, 1 linha) - Todos: Singleton via
globalThis+ graceful shutdown
Decisões de Design e Gotchas
- Ordem de update Swarm: Worker antes do App — BullMQ pode enfileirar jobs com novo payload antes da App estar pronta.
- Postgres no build: Next.js SSG (page generation) precisa de conexão DB. O
build.shsobe o Postgres automaticamente se necessário e para ao final. - Volume
node_modules: Separado do host para evitar incompatibilidade de binários (Alpine vs host). Sempre deletado no./dev.sh build. redis:8-alpinenãoredis:latest: Major version pode mudar. Alpine para imagem mínima.- pgvector no Dockerfile: Usar
pgvector/pgvector:pg17— já vem com a extensão compilada, sem necessidade deapt installextra. - Migrations nunca via
db pushoudb pullem nenhum ambiente. - Conexão fantasma em Docker: Sempre usar heartbeat para Prisma e
keepAlive: truepara pg Pool. Nunca confiar em TCP keepalive via connection string do Prisma.