diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76ee4ce1..50df9152 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,7 +138,7 @@ jobs: TZ: America/Sao_Paulo backend-e2e: - name: Backend e2e (PostgreSQL 16, TZ=${{ matrix.tz }}) + name: Backend e2e (PostgreSQL, TZ=${{ matrix.tz }}) runs-on: ubuntu-latest timeout-minutes: 30 strategy: @@ -150,8 +150,8 @@ jobs: tz: [UTC, America/Sao_Paulo] services: postgres: - # Same major as docker/docker-compose.yml. PostgreSQL 18 lands in v0.2. - image: postgres:16 + # Same image and digest as docker/docker-compose.yml. + image: postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5 env: POSTGRES_USER: myuser POSTGRES_PASSWORD: mypassword diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ffa069c..df3b3eef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ Thanks for your interest in contributing to Thinker! This guide will help you ge | Layer | Technology | | -------- | ------------------------------------------------ | | Frontend | SolidJS, uPlot, custom CSS theme | -| Backend | NestJS 11, TypeORM, PostgreSQL 16 | +| Backend | NestJS 11, TypeORM, PostgreSQL 18 | | Auth | Better Auth (auto-login on localhost) | | Routing | OpenAI-compatible proxy (`/v1/chat/completions`) | | Build | Turborepo + npm workspaces | @@ -70,7 +70,7 @@ docker run --name thinker-postgres \ -e POSTGRES_PASSWORD=mypassword \ -e POSTGRES_DB=mydatabase \ -p 5432:5432 \ - -d postgres:16 + -d postgres:18 ``` That container matches the sample `DATABASE_URL` above: @@ -154,7 +154,7 @@ The backend runs standalone and OpenClaw talks to it as a regular OpenAI-compati ### Backend (`packages/backend`) -- **Framework**: NestJS 11 with TypeORM 0.3 and PostgreSQL 16 +- **Framework**: NestJS 11 with TypeORM 0.3 and PostgreSQL 18 - **Auth**: Better Auth (email/password + Google, GitHub, Discord OAuth) - **Tests**: Jest for unit tests (`*.spec.ts`), Supertest for e2e tests (`test/`) - **Key directories**: `entities/` (data models), `analytics/` (dashboard queries), `routing/` (proxy, scoring, tier assignment), `auth/` (session management) @@ -217,7 +217,7 @@ Write clear, concise commit messages that explain **why** the change was made. U - **Single-service deployment**: In production, NestJS serves both the API and the frontend static files from the same port via `@nestjs/serve-static`. - **Dev mode**: Vite on `:3000` proxies `/api` and `/v1` to the backend on `:3001`. CORS is enabled only in development. -- **Database**: PostgreSQL 16 for both local development and production. Schema changes are managed via TypeORM migrations (`migrationsRun: true` on boot). After modifying an entity, generate a migration with `npm run migration:generate -- src/database/migrations/Name`. +- **Database**: PostgreSQL 18 for both local development and production. Schema changes are managed via TypeORM migrations (`migrationsRun: true` on boot). After modifying an entity, generate a migration with `npm run migration:generate -- src/database/migrations/Name`. - **Validation**: Global `ValidationPipe` with `whitelist: true` and `forbidNonWhitelisted: true`. - **TypeScript**: Strict mode across all packages. diff --git a/deploy/apple-containers/TUTORIAL.md b/deploy/apple-containers/TUTORIAL.md index b005f79d..e164ac1e 100644 --- a/deploy/apple-containers/TUTORIAL.md +++ b/deploy/apple-containers/TUTORIAL.md @@ -189,7 +189,7 @@ THINKER_PG_VOLUME=my-thinker-data ./deploy/apple-containers/start.sh up THINKER_RECORDINGS_VOLUME=my-thinker-recordings ./deploy/apple-containers/start.sh up ``` -The script mounts the named volume at `/var/lib/postgresql/data` and uses `/var/lib/postgresql/data/pgdata` as `PGDATA`. The nested directory is required because a newly formatted Apple Containers volume may contain a root-level `lost+found` directory. +The script mounts the named volume at `/var/lib/postgresql`. PostgreSQL 18 images keep their data in the version-specific `PGDATA` `/var/lib/postgresql/18/docker`, a nested directory, so a root-level `lost+found` directory on a newly formatted Apple Containers volume does not get in the way. A volume created by an earlier version of this script (PostgreSQL 16, data under `pgdata/`) is not migrated: start with a new volume. `down` never deletes either named volume. Use Apple Containers volume commands to inspect or deliberately remove them: @@ -226,11 +226,7 @@ container logs thinker-app ### PostgreSQL stops during initialization -If logs mention `lost+found`, confirm the script sets: - -```text -PGDATA=/var/lib/postgresql/data/pgdata -``` +If logs mention `lost+found`, confirm the volume is mounted at `/var/lib/postgresql` (not at the data directory itself) and that `PGDATA` is not overridden, so PostgreSQL uses its default `/var/lib/postgresql/18/docker`. If logs mention `chmod` or `chown` being denied, do not replace the named volume with a macOS bind mount. The official PostgreSQL image needs filesystem ownership changes that Apple Containers bind mounts do not provide. diff --git a/deploy/apple-containers/start.sh b/deploy/apple-containers/start.sh index 6f20428e..3e1bce4e 100755 --- a/deploy/apple-containers/start.sh +++ b/deploy/apple-containers/start.sh @@ -2,7 +2,7 @@ # Run the Thinker docker-compose stack with Apple Containers (`container` CLI). # # Mirrors docker/docker-compose.yml as closely as Apple Containers allows: -# - postgres:16-alpine with a persistent data directory +# - postgres:18-alpine with a persistent data directory # - thinker app wired to postgres, with health-wait before starting # - host LLM access (Ollama / LM Studio) via the container gateway IP # (Apple Containers has no host.docker.internal) @@ -88,7 +88,7 @@ RECORDINGS_VOLUME="${THINKER_RECORDINGS_VOLUME:-thinker-request-recordings}" # copy of this script run outside the repository. POSTGRES_IMAGE="$(grep -m1 -oE 'postgres:[^[:space:]"]+@sha256:[0-9a-f]{64}' \ "$SCRIPT_DIR/../../docker/docker-compose.yml" 2>/dev/null || true)" -POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea}" +POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5}" if [[ "$POSTGRES_PASSWORD" != "thinker" && -z "${DATABASE_URL:-}" ]]; then echo "error: DATABASE_URL must be set when POSTGRES_PASSWORD is customized." >&2 @@ -232,8 +232,7 @@ cmd_up() { --env POSTGRES_USER=thinker \ --env POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ --env POSTGRES_DB=thinker \ - --env PGDATA=/var/lib/postgresql/data/pgdata \ - --volume "$PG_VOLUME:/var/lib/postgresql/data" \ + --volume "$PG_VOLUME:/var/lib/postgresql" \ "$POSTGRES_IMAGE" fi diff --git a/deploy/coolify/TUTORIAL.md b/deploy/coolify/TUTORIAL.md index 856654d4..42710330 100644 --- a/deploy/coolify/TUTORIAL.md +++ b/deploy/coolify/TUTORIAL.md @@ -39,7 +39,7 @@ curl -sSf https:///api/v1/health ## What Gets Provisioned - Thinker Docker image `tmsoftbrasil/thinker:0.1`. -- PostgreSQL 16 container. +- PostgreSQL 18 container. - Persistent Docker volume for PostgreSQL data. - Persistent Docker volume for compressed request recordings at `/data/request-recordings`. - Generated session, encryption, database password, and public URL values. diff --git a/deploy/coolify/docker-compose.yml b/deploy/coolify/docker-compose.yml index fa457788..66614f4b 100644 --- a/deploy/coolify/docker-compose.yml +++ b/deploy/coolify/docker-compose.yml @@ -33,13 +33,13 @@ services: retries: 3 postgres: - image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea + image: postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5 environment: - POSTGRES_USER=thinker - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - POSTGRES_DB=thinker volumes: - - thinker-postgres-data:/var/lib/postgresql/data + - thinker-postgres-data:/var/lib/postgresql healthcheck: test: pg_isready -U thinker interval: 5s diff --git a/deploy/easypanel/template/README.md b/deploy/easypanel/template/README.md index aaa7432b..5b2d1378 100644 --- a/deploy/easypanel/template/README.md +++ b/deploy/easypanel/template/README.md @@ -16,7 +16,7 @@ templates/thinker/meta.yaml The template provisions: - Thinker app service from `tmsoftbrasil/thinker:0.1`. -- PostgreSQL 16 service. +- PostgreSQL 18 service. - Persistent app volume for compressed request recordings. - Generated database password, Better Auth secret, and Thinker encryption key. - HTTPS domain proxy on port `2099`. diff --git a/deploy/easypanel/template/index.ts b/deploy/easypanel/template/index.ts index 891acd69..1df4b48b 100644 --- a/deploy/easypanel/template/index.ts +++ b/deploy/easypanel/template/index.ts @@ -54,7 +54,7 @@ export function generate(input: Input): Output { type: "postgres", data: { serviceName: input.databaseServiceName, - image: "postgres:16", + image: "postgres:18", password: databasePassword, }, }); diff --git a/docker/DOCKER_README.md b/docker/DOCKER_README.md index 27c83af8..761996c5 100644 --- a/docker/DOCKER_README.md +++ b/docker/DOCKER_README.md @@ -102,6 +102,8 @@ docker compose exec -T postgres pg_dump -U thinker thinker > thinker-backup-$(da `docker compose down -v` apaga tudo. +O PostgreSQL é o 18: o volume `thinker_pgdata` é montado em `/var/lib/postgresql` e os dados ficam em `/var/lib/postgresql/18/docker`. Não há migração de um volume PostgreSQL 16 (não existem instalações anteriores); com um volume antigo o PostgreSQL 18 recusa subir. Nesse caso, faça dump, `down -v`, suba de novo e restaure. + ## Acesso pela rede e proxy reverso O compose publica a porta só em `127.0.0.1`. Para a LAN, ponha `HOST_BIND_ADDRESS=0.0.0.0` e `BETTER_AUTH_URL` com a URL exata do navegador no `.env`. Atrás de proxy reverso, defina `TRUST_PROXY` e uma `BETTER_AUTH_URL` HTTPS. `BETTER_AUTH_URL` precisa ser alcançável de dentro do container (o MCP busca o JWKS nela). Detalhes: [instalação com Docker](https://github.com/patrickbrandao/thinker/blob/main/docs/usuario/instalacao-docker.md). diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8f390ee8..b38639fe 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -212,13 +212,16 @@ services: - frontend postgres: - image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea + image: postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5 environment: - POSTGRES_USER=thinker - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-thinker} - POSTGRES_DB=thinker volumes: - - pgdata:/var/lib/postgresql/data + # PostgreSQL 18+ images keep PGDATA in /var/lib/postgresql/18/docker and + # declare the volume at /var/lib/postgresql, so mount the parent. A PG 16 + # volume (old /var/lib/postgresql/data layout) is not migrated. + - pgdata:/var/lib/postgresql healthcheck: test: pg_isready -U thinker interval: 5s diff --git a/docs/dev/guia.md b/docs/dev/guia.md index d17213af..18f5444f 100644 --- a/docs/dev/guia.md +++ b/docs/dev/guia.md @@ -19,7 +19,7 @@ Monorepo com npm workspaces e Turborepo: ```text packages/ ├── shared/ thinker-shared tipos e helpers usados por backend, frontend e CLI (provedores, tiers, snippets) -├── backend/ thinker-backend NestJS 11, TypeORM 0.3, PostgreSQL 16, Better Auth +├── backend/ thinker-backend NestJS 11, TypeORM 0.3, PostgreSQL 18, Better Auth ├── frontend/ thinker-frontend SolidJS + Vite, dashboard ├── cli/ thinker-cli CLI `thinker` (privada, não publicada) └── thinker/ thinker pacote sem código que guarda a versão canônica @@ -103,7 +103,7 @@ SEED_DATA=true | Backend e2e (Docker) | `npm run test:e2e:docker --workspace=packages/backend` | | Contratos do gateway | `THINKER_BASE_URL=http://127.0.0.1:2099 node --test contracts/gateway/tests/*.test.mjs` | -**E2E só por `test:e2e:docker`.** A suíte usa `dropSchema: true` (apaga o banco). O script `packages/backend/scripts/test-e2e-docker.mjs` escolhe uma porta livre em `127.0.0.1`, sobe um `postgres:16-alpine` com nome único e dados em tmpfs (banco `thinker_e2e_test`), roda as migrações, executa a suíte em série e remove o contêiner no fim (sucesso, falha ou Ctrl-C). Argumentos extras vão para o Jest: +**E2E só por `test:e2e:docker`.** A suíte usa `dropSchema: true` (apaga o banco). O script `packages/backend/scripts/test-e2e-docker.mjs` escolhe uma porta livre em `127.0.0.1`, sobe um `postgres:18-alpine` com nome único e dados em tmpfs montado em `/var/lib/postgresql` (banco `thinker_e2e_test`), roda as migrações, executa a suíte em série e remove o contêiner no fim (sucesso, falha ou Ctrl-C). A imagem é a mesma, com o mesmo digest, de `docker/docker-compose.yml` e do job `Backend e2e (PostgreSQL)` da CI (`THINKER_E2E_PG_IMAGE` troca a imagem). Ao atualizar o digest, atualize os três lugares e `deploy/coolify/docker-compose.yml`. Argumentos extras vão para o Jest: ```bash npm run test:e2e:docker --workspace=packages/backend -- test/limits.e2e-spec.ts @@ -237,7 +237,7 @@ Workflows em `.github/workflows/` (ações fixadas por SHA, token somente leitur | Workflow | Gatilho | Conteúdo | | --- | --- | --- | -| `ci.yml` | PRs e pushes na `main` | Lint, typecheck, build, testes unitários por pacote (o do backend também com `TZ=America/Sao_Paulo`), e2e com PostgreSQL 16 (`TZ=UTC` e `America/Sao_Paulo`), scripts | +| `ci.yml` | PRs e pushes na `main` | Lint, typecheck, build, testes unitários por pacote (o do backend também com `TZ=America/Sao_Paulo`), e2e com PostgreSQL 18 (`TZ=UTC` e `America/Sao_Paulo`), scripts | | `docker.yml` | PRs que tocam a imagem | Build `linux/amd64` e `linux/arm64`, sem publicar | | `docker-smoke.yml` | PRs que tocam imagem ou compose | Teste de fumaça do Compose (somente leitura) e `install.sh` ponta a ponta | | `codeql.yml` | PRs, pushes, semanal | Análise de código | diff --git a/docs/usuario/instalacao-docker.md b/docs/usuario/instalacao-docker.md index 331f8b48..94324b6a 100644 --- a/docs/usuario/instalacao-docker.md +++ b/docs/usuario/instalacao-docker.md @@ -1,6 +1,6 @@ # Instalação com Docker -A instalação oficial do Thinker é Docker/Compose: a imagem `tmsoftbrasil/thinker` (`linux/amd64` e `linux/arm64`) e um PostgreSQL 16. Não há instalação via npm. +A instalação oficial do Thinker é Docker/Compose: a imagem `tmsoftbrasil/thinker` (`linux/amd64` e `linux/arm64`) e um PostgreSQL 18. Não há instalação via npm. ## Requisitos @@ -140,6 +140,12 @@ docker compose up -d `docker compose down` mantém os dados; `docker compose down -v` **apaga tudo**. +## PostgreSQL 18 + +A stack usa o PostgreSQL 18 (`postgres:18-alpine`, fixado por digest). A partir do 18, a imagem oficial guarda os dados em `/var/lib/postgresql/18/docker` e o volume `thinker_pgdata` é montado em `/var/lib/postgresql` (antes era `/var/lib/postgresql/data`). Os volumes novos já nascem com checksums de dados ligados. + +**Não há migração de dados de um volume PostgreSQL 16**: o Thinker não tem instalações anteriores com o 16. Se um volume antigo for montado, o contêiner do PostgreSQL 18 recusa subir (mensagem `there appears to be PostgreSQL data in: /var/lib/postgresql`) em vez de apagar ou ignorar os dados. Para quem testou uma versão anterior: faça o dump com a stack antiga, apague o volume (`docker compose down -v`), suba a stack nova e restaure o dump (veja [Backup](#backup)). + ## Quem vem do Manifest O 0.1 só suporta instalação nova. O projeto Compose, o usuário e o banco do PostgreSQL agora se chamam `thinker`, então os volumes são novos e os dados antigos não aparecem. Detalhes em [Notas de release 0.1.0](../releases/0.1.0.md). diff --git a/packages/backend/scripts/test-e2e-docker.mjs b/packages/backend/scripts/test-e2e-docker.mjs index 6d4686dd..a2bef06e 100644 --- a/packages/backend/scripts/test-e2e-docker.mjs +++ b/packages/backend/scripts/test-e2e-docker.mjs @@ -5,7 +5,7 @@ * npm run test:e2e:docker --workspace=packages/backend [-- ] * * 1. Picks a free TCP port on 127.0.0.1 (never a fixed one like 5432). - * 2. Starts a uniquely named postgres:16-alpine container, data on tmpfs, + * 2. Starts a uniquely named postgres:18-alpine container, data on tmpfs, * database `thinker_e2e_test`, published only on 127.0.0.1:. * 3. Waits until it accepts connections, runs the migrations, then runs the * e2e suite in band with DATABASE_URL and THINKER_E2E_ALLOW_DROP=1 set. @@ -20,7 +20,10 @@ import { createServer } from 'node:net'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -const IMAGE = process.env.THINKER_E2E_PG_IMAGE || 'postgres:16-alpine'; +// Same digest-pinned image as docker/docker-compose.yml and the CI e2e job. +const IMAGE = + process.env.THINKER_E2E_PG_IMAGE || + 'postgres:18-alpine@sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5'; const DB_NAME = 'thinker_e2e_test'; const DB_USER = 'thinker_e2e'; const DB_PASSWORD = randomBytes(12).toString('hex'); @@ -142,7 +145,8 @@ async function startContainer() { '--label', RUN_LABEL, '--tmpfs', - '/var/lib/postgresql/data:rw', + // PostgreSQL 18+ keeps PGDATA under /var/lib/postgresql//docker. + '/var/lib/postgresql:rw', '--publish', `127.0.0.1:${port}:5432`, '--env', diff --git a/packages/backend/test/e2e-db-guard.ts b/packages/backend/test/e2e-db-guard.ts index 9e2590ac..7bc24a24 100644 --- a/packages/backend/test/e2e-db-guard.ts +++ b/packages/backend/test/e2e-db-guard.ts @@ -98,7 +98,7 @@ export function e2eGuardMessage(reason: string): string { '', ' npm run test:e2e:docker --workspace=packages/backend', '', - 'That starts a throwaway postgres:16-alpine container on a free port, runs the', + 'That starts a throwaway postgres:18-alpine container on a free port, runs the', 'migrations and the suite, and removes the container afterwards.', '', 'To use a database you manage yourself, it must be dedicated to tests:', diff --git a/scripts/db-reindex-bloated.sh b/scripts/db-reindex-bloated.sh index 6cddef88..f7cda1b9 100755 --- a/scripts/db-reindex-bloated.sh +++ b/scripts/db-reindex-bloated.sh @@ -13,7 +13,7 @@ # # Usage: # DATABASE_URL=postgresql://...direct, not PgBouncer... scripts/db-reindex-bloated.sh [index ...] -# PSQL="docker run --rm -i --network host postgres:17 psql" DATABASE_URL=... scripts/db-reindex-bloated.sh +# PSQL="docker run --rm -i --network host postgres:18 psql" DATABASE_URL=... scripts/db-reindex-bloated.sh # # With no arguments it rebuilds the default list below, smallest first. It # stops at the first failure after dropping the "_ccnew" leftover, and