diff --git a/.env.example b/.env.example index 8a1fc90..5a892c6 100644 --- a/.env.example +++ b/.env.example @@ -29,6 +29,16 @@ GITEA_DB_USER=gitea GITEA_DB_PASSWORD=troque-essa-senha-forte GITEA_DB_NAME=gitea +# lronetto-finance (repo à parte, consome esta mesma instância de Postgres) +FINANCE_DB_USER=finance +FINANCE_DB_PASSWORD=troque-essa-senha-forte +FINANCE_DB_NAME=finance + +# lronetto-claudeweb (repo à parte, consome esta mesma instância de Postgres) +CLAUDEWEB_DB_USER=claudeweb +CLAUDEWEB_DB_PASSWORD=troque-essa-senha-forte +CLAUDEWEB_DB_NAME=claudeweb + # ----- MinIO (root creds) ----- MINIO_ROOT_USER=minioadmin MINIO_ROOT_PASSWORD=troque-essa-senha-forte @@ -36,6 +46,17 @@ MINIO_ROOT_PASSWORD=troque-essa-senha-forte # Bucket criado já no boot pra ser consumido pela stack wedding_photo WEDDING_BUCKET=wedding-media +# Bucket opcional pra artefatos do lronetto-claudeweb (diff + log de sessão, +# M3/RF-14). Só é usado se o claudeweb tiver S3_ENDPOINT preenchido no seu +# .env; sem isso o app funciona normal e só pula o upload. Ver minio/init.sh. +CLAUDEWEB_BUCKET=claudeweb-artifacts + # ----- pgAdmin (login web) ----- PGADMIN_EMAIL=admin@localhost PGADMIN_PASSWORD=troque-essa-senha-forte + +# ----- scripts/vps-backup.sh (backup completo do host, opcional) ----- +# Só precisa preencher se for usar `make backup-vps`. Todas com default sensato. +# BACKUP_DIR=./backups/vps +# BACKUP_RETENTION_DAYS=14 +# BACKUP_REMOTE= # ex.: r2:meu-bucket/vps-backup (requer rclone configurado) diff --git a/.gitignore b/.gitignore index 519063d..ad3ef1b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ minio/data/ pgadmin/data/ caddy/data/ caddy/config/ + +# Destino do script de backup do VPS (contém dumps + segredos) +backups/ diff --git a/CLAUDE.md b/CLAUDE.md index 95bdd47..4814918 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,33 @@ No disco os 3 ficam como **diretórios irmãos** (ex.: `C:\Users\lrone\code\lron > Histórico: nasceu como monorepo `wedding-app` (branch `main`) com tudo em `infra/{main,gitea,wedding_photo}/`. Foi dividido em 3 repos (fresh start, sem histórico herdado — o monorepo original fica arquivado como backup). +### `lronetto-main` é infra base compartilhada (não só do casamento) + +`lronetto-main` não é infra dedicada ao casamento — é a **infra base** (Postgres + Redis + MinIO + pgAdmin + Caddy + `infra-net`) que **outros serviços independentes** também consomem, além de `gitea` e `wedding`. Todos rodam no mesmo VPS, todos hospedados no mesmo Gitea self-hosted sob o prefixo `lronetto-`: + +| Repo | URL | Escopo | Orquestrado por este `Makefile`? | +|---|---|---|---| +| `lronetto-main` | `https://gitea.lronetto.com/lronetto/lronetto-main.git` | Este repo — infra base (platform layer) + orquestrador | — | +| `lronetto-gitea` | `https://gitea.lronetto.com/lronetto/lronetto-gitea.git` | Git hosting + Actions, hospeda todos os repos `lronetto-*` (dogfooding) | Sim (`up-gitea`) | +| `lronetto-wedding` | `https://gitea.lronetto.com/lronetto/lronetto-wedding.git` | App do casamento (é o que este `CLAUDE.md` documenta em detalhe) | Sim (`up-wedding`) | +| `lronetto-finance` | `https://gitea.lronetto.com/lronetto/lronetto-finance.git` | Open Finance (Pluggy) — outro projeto, não relacionado ao casamento | Sim (`up-finance`) | +| `lronetto-claudeweb` | `https://gitea.lronetto.com/lronetto/lronetto-claudeweb.git` | Interface web pro Claude Code (Agent SDK), single-user — outro projeto, não relacionado ao casamento | Sim (`up-claudeweb`) | + +`lronetto-finance` e `lronetto-claudeweb` **já estão provisionados e rodando em produção**, consumindo Postgres/Redis/MinIO/Caddy deste repo pela `infra-net` — assim como wedding e gitea. Cada um segue o mesmo padrão de "app self-contained, sem Postgres/Redis/Caddy próprios" já usado pela wedding: repo com `docker-compose.yml` (ou `deploy/compose.yml`) próprio, `infra-net` declarada como `external`, containers conectados por nome (`postgres`, `redis`, `minio`), sem porta pública (exposição só via Caddy do main). + +**O que existe hoje neste repo pra suportar os dois:** +- `postgres/init/01-create-databases.sh`: cria roles/databases `finance` e `claudeweb` (além de `wedding`/`gitea`), a partir de `FINANCE_DB_*`/`CLAUDEWEB_DB_*` no `.env` +- `docker-compose.yml` (serviço `postgres`): repassa `FINANCE_DB_*`/`CLAUDEWEB_DB_*` pro init script +- `minio/init.sh`: cria também o bucket `claudeweb-artifacts` (privado, sem download anônimo — usado só se `lronetto-claudeweb` tiver `S3_ENDPOINT` preenchido, pra artefatos de sessão M3/RF-14); `lronetto-finance` não usa MinIO +- `caddy/Caddyfile`: roteia `finance.{DOMAIN_BASE}` → `finance_app:8000` e **`code.{DOMAIN_BASE}`** (não `claudeweb.{DOMAIN_BASE}`!) → `claudeweb-api:8300` (rotas `/api/*`, `/healthz`, `/ws/*`) ou `claudeweb-web:80` (resto, SPA estática) +- `Makefile`: targets `up-finance`/`down-finance`/`logs-finance`/`pull-finance`/`rebuild-finance` e os equivalentes `-claudeweb`, seguindo o padrão de `WEDDING_DIR=`/`GITEA_DIR=` (agora também `FINANCE_DIR=` / `CLAUDEWEB_DIR=`, default `../lronetto-finance` / `../lronetto-claudeweb`) + +**Diferença importante**: `make up`/`down`/`restart`/`status` continuam cobrindo só o **core** (main + gitea + wedding — os 3 repos que este documento detalha). `finance` e `claudeweb` têm targets avulsos (`make up-finance`, `make up-claudeweb`, etc.) porque têm ciclo de vida e deploy CI próprios, independentes deste projeto — não entram no `up`/`down` agregado de propósito. `make status` já lista os dois (com `|| true`, então não quebra se não estiverem clonados/de pé). + +**Gotcha extra do `lronetto-claudeweb`**: ele cria e gerencia por fora uma segunda network Docker, `claudeweb-workers` (não é a `infra-net`), e **conecta manualmente os containers `redis`, `gitea` e (opcionalmente) `minio` deste repo a ela** — é assim que os containers efêmeros de worker (Agent SDK) do claudeweb falam com Redis/Gitea/MinIO sem entrar na `infra-net` (isolamento de egress). Isso significa que `redis`/`gitea`/`minio` ficam com **duas networks anexadas** por fora do `docker-compose.yml` deste repo — um `docker network disconnect` acidental, um recreate de container, ou uma limpeza de networks (`docker network prune`) pode quebrar essa conexão sem avisar aqui. Ver `README.md` do `lronetto-claudeweb` pra recriar (`docker network connect claudeweb-workers `). + +> Nota sobre backup: o `scripts/vps-backup.sh` (seção 8) já cobre `finance`/`claudeweb` de graça — o `pg_dumpall` do passo 1 dumpa **todos** os bancos da instância, e o script agora também copia o `.env` de `lronetto-finance`/`lronetto-claudeweb` (via `FINANCE_DIR=`/`CLAUDEWEB_DIR=`) se estiverem clonados como irmãos. + --- ## 3. Stack técnica @@ -71,7 +98,9 @@ lronetto-main/ # PLATFORM LAYER + ORQUESTRADOR (este repo) ├── caddy/Caddyfile # hostname-based routing ├── postgres/init/01-create-databases.sh # cria DBs wedding + gitea ├── minio/{init.sh, cors.json} # cria bucket wedding-media -└── pgadmin/servers.json # postgres pré-conectado +├── pgadmin/servers.json # postgres pré-conectado +├── scripts/vps-backup.sh # backup completo do host (make backup-vps) +└── backups/vps/ # destino do vps-backup.sh (gitignored) lronetto-gitea/ # GIT + CI ├── .gitignore @@ -124,6 +153,8 @@ lronetto-wedding/ # APLICAÇÃO - `postgres`, `redis`, `minio`, `pgadmin`, `caddy` (stack main) - `gitea`, `gitea_runner` (stack gitea) - `wedding_app`, `wedding_pg_backup`, `wedding_media_backup` (stack wedding_photo) +- `finance_app` (stack `lronetto-finance`, repo à parte) +- `claudeweb-api`, `claudeweb-web` (stack `lronetto-claudeweb`, repo à parte) ### URLs (com `DOMAIN_BASE`) @@ -135,6 +166,8 @@ lronetto-wedding/ # APLICAÇÃO | `https://minio.{DOMAIN_BASE}` | Console admin do MinIO | | `https://media.{DOMAIN_BASE}` | S3 API pública do MinIO (uploads/downloads) | | `ssh://git@gitea.{DOMAIN_BASE}:2222` | Git via SSH | +| `https://finance.{DOMAIN_BASE}` | `lronetto-finance` (Open Finance/Pluggy) — repo à parte, não relacionado ao casamento | +| `https://code.{DOMAIN_BASE}` | `lronetto-claudeweb` (interface web pro Claude Code) — repo à parte, subdomínio é `code`, não `claudeweb` | ### `DOMAIN_BASE` - **Dev local**: `localhost` → Caddy emite cert interno automático pra `*.localhost` @@ -149,24 +182,43 @@ lronetto-wedding/ # APLICAÇÃO ## 6. Comandos ### Orquestrador (`lronetto-main/Makefile`) -Sobe/derruba as 3 stacks de uma vez. Precisa que `lronetto-gitea` e -`lronetto-wedding` estejam como diretórios irmãos (ou ajuste `GITEA_DIR=` / -`WEDDING_DIR=`). +`make up`/`down`/`restart`/`status` cobrem só o **core do casamento** (main + +gitea + wedding — os 3 repos que este documento detalha). Precisa que +`lronetto-gitea` e `lronetto-wedding` estejam como diretórios irmãos (ou +ajuste `GITEA_DIR=` / `WEDDING_DIR=`). + +`finance` e `claudeweb` (seção 2) têm targets avulsos (`up-finance`, +`up-claudeweb`, etc.) — de propósito **não** entram no `up`/`down` agregado, +porque têm ciclo de vida e deploy CI próprios. Ajuste via `FINANCE_DIR=` / +`CLAUDEWEB_DIR=` se não estiverem como diretórios irmãos. + ```bash make help # lista tudo + +# ----- core (main + gitea + wedding) ----- make up # network + main + gitea + wedding (na ordem) make down # inverso make restart # down + up -make status # ps das 3 stacks +make status # ps do core + finance/claudeweb (se de pé) make up-main # só infra base (este repo) make up-gitea make up-wedding # rebuilda imagem do app - make rebuild-wedding # build --no-cache + up make down-{main,gitea,wedding} make logs-{main,gitea,wedding} make pull-{main,gitea,wedding} + +# ----- outros consumidores da infra base (targets avulsos) ----- +make up-finance # build + up do app finance +make up-claudeweb # build da imagem do worker + up do claudeweb (api+web) +make rebuild-{finance,claudeweb} +make down-{finance,claudeweb} +make logs-{finance,claudeweb} +make pull-{finance,claudeweb} + +make backup-vps # roda scripts/vps-backup.sh (backup completo do host, + # inclui finance/claudeweb se clonados como irmãos) ``` ### App wedding (`lronetto-wedding/Makefile`) @@ -262,10 +314,23 @@ make deploy # git reset --hard + up (chamado pelo CI) - `DELETE /api/admin/event/cover` — remove a foto de capa ### Backup + +**Nível stack (`lronetto-wedding`, sidecars sempre ativos)** - **Postgres**: `prodrigestivill/postgres-backup-local` daily, retenção dias/semanas/meses, escreve em `./backups/postgres/` (bind mount do host) - **MinIO**: alpine + mc cron-driven, `mc mirror` (incremental) pra `./backups/media/` - **Backup remoto opcional**: configurar `BACKUP_REMOTE_*` no `.env` da wedding → espelha pra outro endpoint S3 (R2/B2/etc.) +**Nível host (`lronetto-main/scripts/vps-backup.sh`, `make backup-vps`)** +- Script único que cobre o que os sidecars da wedding **não** cobrem: dump lógico completo do Postgres (`pg_dumpall`, todos os bancos + roles, não só o da wedding), snapshot RDB do Redis, tar do diretório de dados do Gitea (repos + config), `.env` das 3 stacks e os certs do Caddy (evita reemissão no Let's Encrypt após um restore) +- Empacota tudo num único `.tar.gz` com permissão `600` (tem segredo dentro — os `.env`) em `backups/vps/`, com rotação por `BACKUP_RETENTION_DAYS` (default 14 dias) +- Envio offsite opcional via `rclone` se `BACKUP_REMOTE` estiver setado (ex.: `r2:meu-bucket/vps-backup`) +- Roda **no host**, fora de containers (usa `docker exec`/`docker cp` pra falar com `postgres`/`redis`, e lê os diretórios dos repos irmãos via `GITEA_DIR=`/`WEDDING_DIR=`, mesma convenção do Makefile orquestrador) +- Pensado pra cron diário (fora de horário de pico): + ```cron + 0 3 * * * cd /opt/lronetto-main && ./scripts/vps-backup.sh >> backups/vps/cron.log 2>&1 + ``` +- Restore é manual (não tem script de restore automático — cada peça volta do seu jeito): `gunzip -c postgres-all.sql.gz | docker exec -i postgres psql -U postgres`, `tar -xzf gitea-data.tar.gz -C ../lronetto-gitea/`, etc. + ### Gitea bootstrap (1ª vez) 1. `make up-main` (precisa estar de pé pro postgres + redis) 2. `make up-gitea` (runner falha porque ainda não tem token, ok) @@ -389,7 +454,7 @@ O `extra_hosts: host-gateway` pra `media.{DOMAIN_BASE}` faz o container resolver 7. **Split em 3 repos**: `lronetto-main` + `lronetto-gitea` + `lronetto-wedding` (fresh start). Orquestrador no main, deploy CI no wedding. Monorepo `wedding-app` arquivado. Repos / branches: -- `lronetto-main`, `lronetto-gitea`, `lronetto-wedding` (atuais; default branch `main`) +- `lronetto-main`, `lronetto-gitea`, `lronetto-wedding` (atuais; default branch `main`; hospedados no Gitea self-hosted da própria stack, prefixo `lronetto-*`) - Arquivados no monorepo `wedding-app`: `claude/wedding-qrcode-photos-C0PQt` (Cloudflare original), `claude/docker-vps-migration` (1ª migração Docker Node) --- @@ -404,7 +469,7 @@ Repos / branches: | Novo bucket no MinIO | `lronetto-main`: `minio/init.sh` + variável no `.env.example` | | Outro DB no postgres | `lronetto-main`: `postgres/init/01-create-databases.sh` + role nova | | Novo subdomínio Caddy | `lronetto-main`: `caddy/Caddyfile` + container_name correspondente | -| Nova app na rede | novo repo com `docker-compose.yml`, declarar `infra-net` como external, conectar a `postgres`/`redis`/`minio` por nome, e referenciar no Makefile orquestrador | +| Nova app na rede | novo repo com `docker-compose.yml`, declarar `infra-net` como external, conectar a `postgres`/`redis`/`minio` por nome, e referenciar no Makefile orquestrador. Exemplos reais já rodando assim: `lronetto-finance` e `lronetto-claudeweb` (seção 2) | | Nova tela no front | `apps/web/src/routes/` + rota em `App.tsx` | | Schema compartilhado front-back | duplica: Zod em `packages/shared/src/schemas.ts` (TS) + Pydantic em `apps/api/app/schemas/api.py` (Python). **Camelo nos dois** | | Workflow CI no Gitea | `.gitea/workflows/*.yml` no repo alvo (sintaxe GitHub Actions) — runner já registrado. Ex.: deploy do app em `lronetto-wedding` | diff --git a/Makefile b/Makefile index c8e05e8..c9c17e7 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,60 @@ .PHONY: help network up down restart status \ - up-main up-gitea up-wedding \ - down-main down-gitea down-wedding \ - logs-main logs-gitea logs-wedding \ - pull-main pull-gitea pull-wedding \ - rebuild-wedding + up-main up-gitea up-wedding up-finance up-claudeweb \ + down-main down-gitea down-wedding down-finance down-claudeweb \ + logs-main logs-gitea logs-wedding logs-finance logs-claudeweb \ + pull-main pull-gitea pull-wedding pull-finance pull-claudeweb \ + rebuild-wedding rebuild-finance rebuild-claudeweb backup-vps -# Orquestrador das 3 stacks. `lronetto-main` é o "platform layer" -# (postgres + redis + minio + pgadmin + caddy). Os repos lronetto-gitea e -# lronetto-wedding são clonados como diretórios IRMÃOS e referenciados aqui. +# Orquestrador do "core" das stacks do casamento (main + gitea + wedding, os 3 +# repos que este CLAUDE.md documenta em detalhe). `lronetto-main` é o +# "platform layer" (postgres + redis + minio + pgadmin + caddy) e também serve +# de infra base pra OUTROS consumidores não relacionados ao casamento +# (lronetto-finance, lronetto-claudeweb) — ver seção 2 do CLAUDE.md. +# +# `make up`/`down`/`restart`/`status` cobrem só o core (main+gitea+wedding), +# de propósito: finance/claudeweb têm ciclo de vida e deploy CI próprios, +# independentes deste projeto. Os targets up-finance/up-claudeweb existem só +# de conveniência operacional (ex.: subir tudo depois de um reboot do VPS). +# +# Todos os repos são clonados como diretórios IRMÃOS e referenciados aqui. # Ajuste os caminhos abaixo se usar outro layout no disco. NET := infra-net -WEDDING_DIR ?= ../lronetto-wedding -GITEA_DIR ?= ../lronetto-gitea +WEDDING_DIR ?= ../lronetto-wedding +GITEA_DIR ?= ../lronetto-gitea +FINANCE_DIR ?= ../lronetto-finance +CLAUDEWEB_DIR ?= ../lronetto-claudeweb -DC_MAIN := docker compose -f docker-compose.yml --env-file .env -DC_GITEA := docker compose -f $(GITEA_DIR)/docker-compose.yml --env-file $(GITEA_DIR)/.env -DC_WEDDING := docker compose -f $(WEDDING_DIR)/docker-compose.yml --env-file $(WEDDING_DIR)/.env +DC_MAIN := docker compose -f docker-compose.yml --env-file .env +DC_GITEA := docker compose -f $(GITEA_DIR)/docker-compose.yml --env-file $(GITEA_DIR)/.env +DC_WEDDING := docker compose -f $(WEDDING_DIR)/docker-compose.yml --env-file $(WEDDING_DIR)/.env +DC_FINANCE := docker compose -f $(FINANCE_DIR)/docker-compose.yml --env-file $(FINANCE_DIR)/.env +DC_CLAUDEWEB := docker compose -f $(CLAUDEWEB_DIR)/deploy/compose.yml --env-file $(CLAUDEWEB_DIR)/.env +CLAUDEWEB_WORKER_IMAGE ?= claudeweb-worker help: - @echo "Stacks: main (este repo) | gitea ($(GITEA_DIR)) | wedding ($(WEDDING_DIR))" + @echo "Core (orquestrado por make up/down/restart/status):" + @echo " main (este repo) | gitea ($(GITEA_DIR)) | wedding ($(WEDDING_DIR))" @echo "" - @echo " make up Sobe tudo na ordem (main -> gitea -> wedding)" - @echo " make down Desce tudo na ordem inversa" + @echo "Outros consumidores da infra base (targets avulsos, ciclo de vida próprio):" + @echo " finance ($(FINANCE_DIR)) | claudeweb ($(CLAUDEWEB_DIR))" + @echo "" + @echo " make up Sobe o core na ordem (main -> gitea -> wedding)" + @echo " make down Desce o core na ordem inversa" @echo " make restart down + up" - @echo " make status ps de todas as stacks" + @echo " make status ps do core + finance/claudeweb (se de pé)" @echo "" @echo " make up-main Sobe só a stack main" @echo " make up-gitea Sobe só o gitea" @echo " make up-wedding Build + up do app wedding" - @echo " make rebuild-wedding Force rebuild do app wedding" - @echo " make down-{main,gitea,wedding}" - @echo " make logs-{main,gitea,wedding}" - @echo " make pull-{main,gitea,wedding}" + @echo " make up-finance Build + up do app finance" + @echo " make up-claudeweb Build da imagem do worker + up do claudeweb (api+web)" + @echo " make rebuild-{wedding,finance,claudeweb} Force rebuild" + @echo " make down-{main,gitea,wedding,finance,claudeweb}" + @echo " make logs-{main,gitea,wedding,finance,claudeweb}" + @echo " make pull-{main,gitea,wedding,finance,claudeweb}" @echo " make network Cria a network external '$(NET)' (idempotente)" + @echo " make backup-vps Backup completo do host (postgres+redis+gitea+envs+caddy certs)" network: @./network.sh @@ -48,6 +69,8 @@ status: @$(DC_MAIN) ps || true @$(DC_GITEA) ps || true @$(DC_WEDDING) ps || true + @$(DC_FINANCE) ps || true + @$(DC_CLAUDEWEB) ps || true up-main: network $(DC_MAIN) up -d @@ -58,10 +81,26 @@ up-gitea: network up-wedding: network $(DC_WEDDING) up -d --build +up-finance: network + $(DC_FINANCE) up -d --build + +up-claudeweb: network + docker build -t $(CLAUDEWEB_WORKER_IMAGE) -f $(CLAUDEWEB_DIR)/worker/Dockerfile $(CLAUDEWEB_DIR) + $(DC_CLAUDEWEB) up -d --build + rebuild-wedding: network $(DC_WEDDING) build --no-cache $(DC_WEDDING) up -d +rebuild-finance: network + $(DC_FINANCE) build --no-cache + $(DC_FINANCE) up -d + +rebuild-claudeweb: network + docker build -t $(CLAUDEWEB_WORKER_IMAGE) -f $(CLAUDEWEB_DIR)/worker/Dockerfile $(CLAUDEWEB_DIR) + $(DC_CLAUDEWEB) build --no-cache + $(DC_CLAUDEWEB) up -d + down-main: $(DC_MAIN) down @@ -71,6 +110,12 @@ down-gitea: down-wedding: $(DC_WEDDING) down +down-finance: + $(DC_FINANCE) down + +down-claudeweb: + $(DC_CLAUDEWEB) down + logs-main: $(DC_MAIN) logs -f @@ -80,6 +125,12 @@ logs-gitea: logs-wedding: $(DC_WEDDING) logs -f +logs-finance: + $(DC_FINANCE) logs -f + +logs-claudeweb: + $(DC_CLAUDEWEB) logs -f + pull-main: $(DC_MAIN) pull @@ -88,3 +139,12 @@ pull-gitea: pull-wedding: $(DC_WEDDING) pull + +pull-finance: + $(DC_FINANCE) pull + +pull-claudeweb: + $(DC_CLAUDEWEB) pull + +backup-vps: + @GITEA_DIR=$(GITEA_DIR) WEDDING_DIR=$(WEDDING_DIR) FINANCE_DIR=$(FINANCE_DIR) CLAUDEWEB_DIR=$(CLAUDEWEB_DIR) ./scripts/vps-backup.sh diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 12a0245..f3d292e 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -29,6 +29,34 @@ wedding.{$DOMAIN_BASE} { } } +# ----- Finance (lronetto-finance, repo à parte) ----- +finance.{$DOMAIN_BASE} { + encode gzip zstd + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + Referrer-Policy "strict-origin-when-cross-origin" + Permissions-Policy "geolocation=(), microphone=(), camera=()" + Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.pluggy.ai; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.pluggy.ai; frame-src https://connect.pluggy.ai; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" + -Server + } + reverse_proxy finance_app:8000 +} + +# ----- Claudeweb (lronetto-claudeweb, repo à parte) — subdomínio é "code", não "claudeweb" ----- +code.{$DOMAIN_BASE} { + encode zstd gzip + + @backend path /api/* /healthz /ws/* + handle @backend { + reverse_proxy claudeweb-api:8300 + } + + handle { + reverse_proxy claudeweb-web:80 + } +} + # ----- pgAdmin ----- pgadmin.{$DOMAIN_BASE} { encode gzip diff --git a/docker-compose.yml b/docker-compose.yml index ba028e2..3085a61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,12 @@ services: GITEA_DB_USER: ${GITEA_DB_USER:-gitea} GITEA_DB_PASSWORD: ${GITEA_DB_PASSWORD} GITEA_DB_NAME: ${GITEA_DB_NAME:-gitea} + FINANCE_DB_USER: ${FINANCE_DB_USER:-finance} + FINANCE_DB_PASSWORD: ${FINANCE_DB_PASSWORD} + FINANCE_DB_NAME: ${FINANCE_DB_NAME:-finance} + CLAUDEWEB_DB_USER: ${CLAUDEWEB_DB_USER:-claudeweb} + CLAUDEWEB_DB_PASSWORD: ${CLAUDEWEB_DB_PASSWORD} + CLAUDEWEB_DB_NAME: ${CLAUDEWEB_DB_NAME:-claudeweb} volumes: - ./postgres/data:/var/lib/postgresql/data - ./postgres/init:/docker-entrypoint-initdb.d:ro @@ -69,6 +75,7 @@ services: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} WEDDING_BUCKET: ${WEDDING_BUCKET:-wedding-media} + CLAUDEWEB_BUCKET: ${CLAUDEWEB_BUCKET:-claudeweb-artifacts} volumes: - ./minio/init.sh:/init.sh:ro - ./minio/cors.json:/cors.json:ro diff --git a/minio/init.sh b/minio/init.sh index a62532d..23c16fd 100644 --- a/minio/init.sh +++ b/minio/init.sh @@ -1,13 +1,20 @@ #!/bin/sh -# Inicializa o bucket usado pela stack wedding_photo. Roda como container -# one-shot depois que o MinIO ficou healthy. +# Inicializa os buckets consumidos pelas stacks. Roda como container one-shot +# depois que o MinIO ficou healthy. set -e mc alias set --quiet local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" +# ----- wedding: bucket público (download anônimo) pra fotos/vídeos dos convidados ----- mc mb --ignore-existing "local/$WEDDING_BUCKET" mc anonymous set download "local/$WEDDING_BUCKET" mc cors set "local/$WEDDING_BUCKET" /cors.json 2>/dev/null || \ echo "[minio-init] cors set ignorado (versão antiga do mc?)" - echo "[minio-init] bucket '$WEDDING_BUCKET' pronto (anonymous download + CORS)" + +# ----- claudeweb: bucket privado pra artefatos de sessão (diff + log, M3/RF-14). +# Opcional: o app só sobe artefatos se S3_ENDPOINT estiver preenchido no seu +# .env; sem isso este bucket fica criado mas sem uso. Sem download anônimo +# (diferente do wedding-media) porque o conteúdo não é público. +mc mb --ignore-existing "local/${CLAUDEWEB_BUCKET:-claudeweb-artifacts}" +echo "[minio-init] bucket '${CLAUDEWEB_BUCKET:-claudeweb-artifacts}' pronto (privado)" diff --git a/postgres/init/01-create-databases.sh b/postgres/init/01-create-databases.sh index b5a5b71..16f10b9 100644 --- a/postgres/init/01-create-databases.sh +++ b/postgres/init/01-create-databases.sh @@ -12,6 +12,14 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E CREATE ROLE "${GITEA_DB_USER}" WITH LOGIN PASSWORD '${GITEA_DB_PASSWORD}'; CREATE DATABASE "${GITEA_DB_NAME}" OWNER "${GITEA_DB_USER}"; GRANT ALL PRIVILEGES ON DATABASE "${GITEA_DB_NAME}" TO "${GITEA_DB_USER}"; + + CREATE ROLE "${FINANCE_DB_USER}" WITH LOGIN PASSWORD '${FINANCE_DB_PASSWORD}'; + CREATE DATABASE "${FINANCE_DB_NAME}" OWNER "${FINANCE_DB_USER}"; + GRANT ALL PRIVILEGES ON DATABASE "${FINANCE_DB_NAME}" TO "${FINANCE_DB_USER}"; + + CREATE ROLE "${CLAUDEWEB_DB_USER}" WITH LOGIN PASSWORD '${CLAUDEWEB_DB_PASSWORD}'; + CREATE DATABASE "${CLAUDEWEB_DB_NAME}" OWNER "${CLAUDEWEB_DB_USER}"; + GRANT ALL PRIVILEGES ON DATABASE "${CLAUDEWEB_DB_NAME}" TO "${CLAUDEWEB_DB_USER}"; EOSQL -echo "[postgres-init] roles + databases criados para wedding (${WEDDING_DB_NAME}) e gitea (${GITEA_DB_NAME})" +echo "[postgres-init] roles + databases criados para wedding (${WEDDING_DB_NAME}), gitea (${GITEA_DB_NAME}), finance (${FINANCE_DB_NAME}) e claudeweb (${CLAUDEWEB_DB_NAME})" diff --git a/scripts/vps-backup.sh b/scripts/vps-backup.sh new file mode 100755 index 0000000..76b40fc --- /dev/null +++ b/scripts/vps-backup.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# Backup completo do VPS (nível host, fora dos sidecars de cada stack). +# +# Cobre o que os sidecars de `lronetto-wedding` (postgres-backup-local + +# media-backup) NÃO cobrem sozinhos: Redis, dados do Gitea (repos + config), +# .env de todos os consumidores da infra base (wedding, gitea, finance, +# claudeweb) e certs do Caddy. Pensado pra rodar via cron no host, fora de +# qualquer container. +# +# Nota: o dump do Postgres é `pg_dumpall` (passo 1) — cobre TODOS os bancos da +# instância de uma vez (wedding, gitea, finance, claudeweb), não só o da +# wedding. Não precisa de um passo por app. +# +# Uso: +# ./scripts/vps-backup.sh +# +# Cron sugerido (diário às 3h, log em backups/vps/cron.log): +# 0 3 * * * cd /opt/lronetto-main && ./scripts/vps-backup.sh >> backups/vps/cron.log 2>&1 +# +# Env vars (todas opcionais, com default): +# BACKUP_DIR destino local dos tarballs (default: ./backups/vps) +# BACKUP_RETENTION_DAYS dias de retenção local (default: 14) +# BACKUP_REMOTE remote do rclone pra cópia offsite, ex.: r2:meu-bucket/vps-backup +# (vazio = não envia offsite) +# GITEA_DIR caminho do repo lronetto-gitea (default: ../lronetto-gitea) +# WEDDING_DIR caminho do repo lronetto-wedding (default: ../lronetto-wedding) +# FINANCE_DIR caminho do repo lronetto-finance (default: ../lronetto-finance) +# CLAUDEWEB_DIR caminho do repo lronetto-claudeweb (default: ../lronetto-claudeweb) +# MINIO_ROOT_USER / MINIO_ROOT_PASSWORD só necessários se quiser o mirror +# direto do MinIO por aqui (ver passo 4) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +BACKUP_DIR="${BACKUP_DIR:-$MAIN_DIR/backups/vps}" +BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}" +BACKUP_REMOTE="${BACKUP_REMOTE:-}" +GITEA_DIR="${GITEA_DIR:-$MAIN_DIR/../lronetto-gitea}" +WEDDING_DIR="${WEDDING_DIR:-$MAIN_DIR/../lronetto-wedding}" +FINANCE_DIR="${FINANCE_DIR:-$MAIN_DIR/../lronetto-finance}" +CLAUDEWEB_DIR="${CLAUDEWEB_DIR:-$MAIN_DIR/../lronetto-claudeweb}" + +TS="$(date +%Y%m%d-%H%M%S)" +WORK_DIR="$(mktemp -d)" +STAGE="$WORK_DIR/vps-backup-$TS" +mkdir -p "$STAGE" + +log() { echo "[vps-backup] $(date '+%Y-%m-%d %H:%M:%S') $*"; } +cleanup() { rm -rf "$WORK_DIR"; } +trap cleanup EXIT + +container_up() { + docker ps --format '{{.Names}}' | grep -qx "$1" +} + +# 1. Postgres: dump lógico de TODOS os bancos + roles (pg_dumpall) +if container_up postgres; then + log "Dump do Postgres (pg_dumpall)..." + docker exec -i postgres pg_dumpall -U postgres | gzip > "$STAGE/postgres-all.sql.gz" +else + log "AVISO: container 'postgres' não está rodando — pulando dump, backup incompleto" +fi + +# 2. Redis: snapshot RDB (BGSAVE + copia o dump.rdb gerado) +if container_up redis; then + log "Snapshot do Redis (BGSAVE)..." + docker exec redis redis-cli BGSAVE >/dev/null + for _ in $(seq 1 30); do + sleep 1 + docker exec redis redis-cli INFO persistence 2>/dev/null | grep -q 'rdb_bgsave_in_progress:0' && break + done + docker cp redis:/data/dump.rdb "$STAGE/redis-dump.rdb" 2>/dev/null \ + || log "AVISO: não consegui copiar dump.rdb do redis" +else + log "AVISO: container 'redis' não está rodando — pulando snapshot" +fi + +# 3. Gitea: repos + config + dados (tudo que o docker-compose do gitea monta em bind mount) +if [ -d "$GITEA_DIR" ]; then + if [ -d "$GITEA_DIR/gitea/data" ]; then + log "Compactando dados do Gitea ($GITEA_DIR/gitea/data)..." + tar -czf "$STAGE/gitea-data.tar.gz" -C "$GITEA_DIR" gitea/data + else + log "AVISO: $GITEA_DIR/gitea/data não encontrado (confira o bind mount no docker-compose.yml do gitea)" + fi +else + log "AVISO: $GITEA_DIR não encontrado — pulando backup do Gitea (ajuste GITEA_DIR= se o caminho for outro)" +fi + +# 4. MinIO: mirror incremental do bucket de mídia via mc (opcional — a stack wedding já +# tem o sidecar media-backup fazendo isso continuamente; isso aqui é um extra/fallback +# se você rodar esse script num host que não tem o sidecar, ou quiser um snapshot avulso) +if command -v mc >/dev/null 2>&1 && container_up minio; then + log "Mirror do MinIO (bucket ${WEDDING_BUCKET:-wedding-media})..." + mc alias set vps-backup-src "http://localhost:9000" \ + "${MINIO_ROOT_USER:?defina MINIO_ROOT_USER pra fazer o mirror do MinIO, ou remova o binário mc do PATH pra pular essa etapa}" \ + "${MINIO_ROOT_PASSWORD:?defina MINIO_ROOT_PASSWORD pra fazer o mirror do MinIO}" >/dev/null + mkdir -p "$STAGE/minio-media" + mc mirror --quiet "vps-backup-src/${WEDDING_BUCKET:-wedding-media}" "$STAGE/minio-media" \ + || log "AVISO: mc mirror falhou" + mc alias remove vps-backup-src >/dev/null 2>&1 || true +else + log "mc indisponível (ou minio parado) — pulando mirror direto; confie no sidecar media-backup da stack wedding" +fi + +# 5. .env de todos os consumidores da infra base (segredos!) — o tarball final +# fica com permissão 600. Cobre os repos "core" (wedding/gitea) e os demais +# consumidores do platform layer (finance/claudeweb), se estiverem clonados. +log "Copiando .env de todos os repos..." +mkdir -p "$STAGE/envs" +[ -f "$MAIN_DIR/.env" ] && cp "$MAIN_DIR/.env" "$STAGE/envs/lronetto-main.env" +[ -f "$GITEA_DIR/.env" ] && cp "$GITEA_DIR/.env" "$STAGE/envs/lronetto-gitea.env" +[ -f "$WEDDING_DIR/.env" ] && cp "$WEDDING_DIR/.env" "$STAGE/envs/lronetto-wedding.env" +[ -f "$FINANCE_DIR/.env" ] && cp "$FINANCE_DIR/.env" "$STAGE/envs/lronetto-finance.env" +[ -f "$CLAUDEWEB_DIR/.env" ] && cp "$CLAUDEWEB_DIR/.env" "$STAGE/envs/lronetto-claudeweb.env" + +# 6. Certs do Caddy (evita reemissão no Let's Encrypt depois de um restore) +if [ -d "$MAIN_DIR/caddy/data" ]; then + log "Compactando certs do Caddy..." + tar -czf "$STAGE/caddy-data.tar.gz" -C "$MAIN_DIR/caddy" data +fi + +# 7. Empacota tudo num único tarball final, permissão restrita (tem segredo dentro) +mkdir -p "$BACKUP_DIR" +FINAL="$BACKUP_DIR/vps-backup-$TS.tar.gz" +log "Gerando tarball final: $FINAL" +tar -czf "$FINAL" -C "$WORK_DIR" "vps-backup-$TS" +chmod 600 "$FINAL" + +# 8. Envia offsite via rclone, se configurado (BACKUP_REMOTE + rclone instalado) +if [ -n "$BACKUP_REMOTE" ]; then + if command -v rclone >/dev/null 2>&1; then + log "Enviando offsite pra $BACKUP_REMOTE..." + rclone copy "$FINAL" "$BACKUP_REMOTE" || log "AVISO: rclone copy falhou" + else + log "AVISO: BACKUP_REMOTE definido mas rclone não está instalado — pulando envio offsite" + fi +fi + +# 9. Rotação: apaga tarballs locais mais velhos que a retenção configurada +log "Rotacionando backups locais (> $BACKUP_RETENTION_DAYS dias)..." +find "$BACKUP_DIR" -name 'vps-backup-*.tar.gz' -mtime "+$BACKUP_RETENTION_DAYS" -print -delete + +log "Backup concluído: $FINAL ($(du -h "$FINAL" | cut -f1))"