chore: initial commit (lronetto-main split do monorepo wedding-app)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
c1f8b12d3c
41
.env.example
Normal file
41
.env.example
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# infra/main/.env — copie pra .env e edite
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ----- Domínio + TLS -----
|
||||||
|
# Dev local: deixa "localhost". Caddy emite cert interno automático pra *.localhost
|
||||||
|
# Prod: usa teu domínio (ex.: lronetto.com.br). Apontar DNS:
|
||||||
|
# wedding.SEU.DOMINIO → IP do VPS
|
||||||
|
# gitea.SEU.DOMINIO → IP do VPS
|
||||||
|
# media.SEU.DOMINIO → IP do VPS
|
||||||
|
# pgadmin.SEU.DOMINIO → IP do VPS
|
||||||
|
# minio.SEU.DOMINIO → IP do VPS
|
||||||
|
DOMAIN_BASE=localhost
|
||||||
|
|
||||||
|
# Em produção: email pra Let's Encrypt renovar certs. Em dev pode ficar vazio.
|
||||||
|
ACME_EMAIL=
|
||||||
|
|
||||||
|
# Timezone (aplica em postgres-backup, cron de media-backup, etc.)
|
||||||
|
TZ=America/Sao_Paulo
|
||||||
|
|
||||||
|
# ----- Postgres: admin + bancos por app -----
|
||||||
|
POSTGRES_ADMIN_PASSWORD=troque-essa-senha-forte
|
||||||
|
|
||||||
|
WEDDING_DB_USER=wedding
|
||||||
|
WEDDING_DB_PASSWORD=troque-essa-senha-forte
|
||||||
|
WEDDING_DB_NAME=wedding
|
||||||
|
|
||||||
|
GITEA_DB_USER=gitea
|
||||||
|
GITEA_DB_PASSWORD=troque-essa-senha-forte
|
||||||
|
GITEA_DB_NAME=gitea
|
||||||
|
|
||||||
|
# ----- MinIO (root creds) -----
|
||||||
|
MINIO_ROOT_USER=minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD=troque-essa-senha-forte
|
||||||
|
|
||||||
|
# Bucket criado já no boot pra ser consumido pela stack wedding_photo
|
||||||
|
WEDDING_BUCKET=wedding-media
|
||||||
|
|
||||||
|
# ----- pgAdmin (login web) -----
|
||||||
|
PGADMIN_EMAIL=admin@localhost
|
||||||
|
PGADMIN_PASSWORD=troque-essa-senha-forte
|
||||||
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Bind mounts dos volumes Docker
|
||||||
|
postgres/data/
|
||||||
|
redis/data/
|
||||||
|
minio/data/
|
||||||
|
pgadmin/data/
|
||||||
|
caddy/data/
|
||||||
|
caddy/config/
|
||||||
422
CLAUDE.md
Normal file
422
CLAUDE.md
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Notas de projeto pro Claude Code (e qualquer dev que entre depois). Resume todas as decisões tomadas ao longo das conversas. Linguagem do projeto: **PT-BR**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visão geral
|
||||||
|
|
||||||
|
App web pro casamento de **Stefanie & Leandro**. Convidados escaneiam um QR Code na mesa, abrem o site, mandam fotos/vídeos + uma mensagem. Os noivos administram tudo num painel `/admin`.
|
||||||
|
|
||||||
|
**Escopo**: single-tenant (um casamento). Multi-tenant (SaaS) foi avaliado — fica pra um pivot futuro se houver demanda real (3+ pedidos).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Arquitetura: 3 repositórios / 3 stacks Docker Compose
|
||||||
|
|
||||||
|
Cada stack é um repositório git separado, com seu próprio `docker-compose.yml` isolado. Os 3 compartilham uma **network Docker externa** chamada `infra-net`.
|
||||||
|
|
||||||
|
```
|
||||||
|
lronetto-main/ Postgres + Redis + MinIO + pgAdmin + Caddy (+ orquestrador)
|
||||||
|
lronetto-gitea/ Gitea + Actions runner
|
||||||
|
lronetto-wedding/ App FastAPI + sidecars de backup (+ deploy CI)
|
||||||
|
```
|
||||||
|
|
||||||
|
No disco os 3 ficam como **diretórios irmãos** (ex.: `C:\Users\lrone\code\lronetto-*`). O `Makefile` orquestrador (neste repo, `lronetto-main`) referencia os outros dois por caminho relativo (`../lronetto-gitea`, `../lronetto-wedding`) — sobrescrevível via `GITEA_DIR=` / `WEDDING_DIR=`.
|
||||||
|
|
||||||
|
**Por que repos separados**: cada stack tem ciclo de vida, histórico e CI próprios. Subir/derrubar ou versionar uma não afeta as outras. Gitea pode ser desativado sem tocar no app.
|
||||||
|
|
||||||
|
**`lronetto-main` é o "platform layer"** que os consumidores usam. `gitea` e `wedding` conectam ao Postgres e MinIO de lá pela `infra-net`.
|
||||||
|
|
||||||
|
> 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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Stack técnica
|
||||||
|
|
||||||
|
| Camada | Escolha | Por quê |
|
||||||
|
|---|---|---|
|
||||||
|
| HTTP framework | **FastAPI 0.115** | Async-first, OpenAPI auto, Pydantic v2 |
|
||||||
|
| ASGI server | **uvicorn** | Padrão de fato |
|
||||||
|
| ORM | **SQLAlchemy 2.0 async + asyncpg** | Tipos modernos, drivers async maduros |
|
||||||
|
| Validação | **Pydantic v2** | Built-in no FastAPI; camelCase no wire |
|
||||||
|
| Env loader | **pydantic-settings** | Falha rápido em var faltando |
|
||||||
|
| Storage SDK | **boto3** wrapped em `asyncio.to_thread` | S3 compatível, lida com MinIO + R2 + B2 idem |
|
||||||
|
| Auth | **pyjwt** + cookie HttpOnly | 7 dias, HS256, constant-time compare |
|
||||||
|
| QR codes | **qrcode** + **reportlab** | PNG/SVG/PDF A6 imprimível |
|
||||||
|
| HEIC | **pillow-heif** | Decode iPhone HEIC → JPEG no `/confirm` |
|
||||||
|
| Frontend | **Vite + React 18 + Tailwind + react-router** | Stack comum, rápida |
|
||||||
|
| DB | **Postgres 16** | Multi-banco em uma instância (wedding + gitea) |
|
||||||
|
| Cache | **Redis 7** | Gitea usa hoje (cache + sessões); wedding pode usar depois |
|
||||||
|
| Storage | **MinIO** | S3 compat self-hosted; código portável pra R2/B2 |
|
||||||
|
| Reverse proxy | **Caddy 2** | TLS auto (Let's Encrypt em prod, interno em `*.localhost` dev) |
|
||||||
|
| Git hosting | **Gitea 1.22** + Actions runner | Self-hosted, leve, compatível com GitHub Actions |
|
||||||
|
| Backup | `prodrigestivill/postgres-backup-local` + `alpine + mc` | Sidecars com cron, rotação dias/semanas/meses |
|
||||||
|
| Package mgmt | **uv** (Python), **pnpm** (Node) | Mais rápidos que pip/npm |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Estrutura de diretórios completa
|
||||||
|
|
||||||
|
Três repos irmãos no mesmo diretório pai:
|
||||||
|
|
||||||
|
```
|
||||||
|
lronetto-main/ # PLATFORM LAYER + ORQUESTRADOR (este repo)
|
||||||
|
├── .gitignore
|
||||||
|
├── Makefile # Orquestra as 3 stacks (refs ../lronetto-{gitea,wedding})
|
||||||
|
├── network.sh # Cria infra-net (idempotente)
|
||||||
|
├── CLAUDE.md # Este arquivo
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── .env.example
|
||||||
|
├── 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
|
||||||
|
|
||||||
|
lronetto-gitea/ # GIT + CI
|
||||||
|
├── .gitignore
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── .env.example
|
||||||
|
└── runner/Dockerfile # act_runner + docker-cli
|
||||||
|
|
||||||
|
lronetto-wedding/ # APLICAÇÃO
|
||||||
|
├── .gitignore
|
||||||
|
├── docker-compose.yml # app + postgres-backup + media-backup
|
||||||
|
├── .env.example
|
||||||
|
├── Dockerfile # multi-stage Node(build web) + Python(runtime)
|
||||||
|
├── Makefile # dev local (uv, pnpm) + ops da stack + deploy
|
||||||
|
├── .gitea/workflows/deploy.yml # CI: push na main -> SSH no host -> make deploy
|
||||||
|
├── pyproject.toml, package.json, pnpm-workspace.yaml, ...
|
||||||
|
├── apps/
|
||||||
|
│ ├── api/ # FastAPI Python
|
||||||
|
│ │ ├── pyproject.toml
|
||||||
|
│ │ └── app/
|
||||||
|
│ │ ├── main.py # FastAPI app, lifespan, SPA fallback
|
||||||
|
│ │ ├── config.py # pydantic-settings
|
||||||
|
│ │ ├── db/{base,models,migrate}.py
|
||||||
|
│ │ ├── migrations/ # SQL puro, runner idempotente
|
||||||
|
│ │ ├── lib/ # auth, storage, qrcode, pdf, ids, transcode
|
||||||
|
│ │ ├── schemas/api.py # Pydantic v2 wire models
|
||||||
|
│ │ └── routes/{public,uploads,admin}.py
|
||||||
|
│ └── web/ # Vite + React + Tailwind SPA
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── vite.config.ts
|
||||||
|
│ ├── tailwind.config.ts
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main.tsx, App.tsx
|
||||||
|
│ ├── routes/{Home,Upload,Gallery}.tsx + admin/{Login,Dashboard}.tsx
|
||||||
|
│ └── lib/{api,upload}.ts
|
||||||
|
├── packages/shared/ # Zod schemas TS (usado pelo web)
|
||||||
|
├── infra/backup/ # entrypoint + script do media-backup
|
||||||
|
└── backups/ # destino dos sidecars (gitignored)
|
||||||
|
├── postgres/
|
||||||
|
└── media/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Network e roteamento
|
||||||
|
|
||||||
|
### Network
|
||||||
|
`infra-net` é **external**. Criada pelo `network.sh` (chamada por `make network`). Containers de stacks diferentes se enxergam por nome via DNS interno do Docker.
|
||||||
|
|
||||||
|
### Container names fixos
|
||||||
|
- `postgres`, `redis`, `minio`, `pgadmin`, `caddy` (stack main)
|
||||||
|
- `gitea`, `gitea_runner` (stack gitea)
|
||||||
|
- `wedding_app`, `wedding_pg_backup`, `wedding_media_backup` (stack wedding_photo)
|
||||||
|
|
||||||
|
### URLs (com `DOMAIN_BASE`)
|
||||||
|
|
||||||
|
| Hostname | Serve |
|
||||||
|
|---|---|
|
||||||
|
| `https://wedding.{DOMAIN_BASE}` | Site dos noivos (uploads + galeria + admin) |
|
||||||
|
| `https://gitea.{DOMAIN_BASE}` | Git hosting + Actions |
|
||||||
|
| `https://pgadmin.{DOMAIN_BASE}` | Web UI dos bancos |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
|
### `DOMAIN_BASE`
|
||||||
|
- **Dev local**: `localhost` → Caddy emite cert interno automático pra `*.localhost`
|
||||||
|
- **Produção**: domínio real (ex.: `lronetto.com`) → Let's Encrypt automático
|
||||||
|
- Mudar `DOMAIN_BASE` requer **down + up** das 3 stacks (envs lidos no boot)
|
||||||
|
|
||||||
|
### Hairpin / `extra_hosts`
|
||||||
|
`wedding_app` tem `extra_hosts: media.{DOMAIN_BASE}:host-gateway` e `wedding.{DOMAIN_BASE}:host-gateway` pra que, mesmo dentro do container, ele resolva esses hostnames pro Docker host gateway → Caddy. Assim assinaturas S3 fecham (signing host == host que o browser usa pra PUT).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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=`).
|
||||||
|
```bash
|
||||||
|
make help # lista tudo
|
||||||
|
make up # network + main + gitea + wedding (na ordem)
|
||||||
|
make down # inverso
|
||||||
|
make restart # down + up
|
||||||
|
make status # ps das 3 stacks
|
||||||
|
|
||||||
|
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}
|
||||||
|
```
|
||||||
|
|
||||||
|
### App wedding (`lronetto-wedding/Makefile`)
|
||||||
|
Repo self-contained — roda standalone, sem o orquestrador.
|
||||||
|
```bash
|
||||||
|
# Dev local (fora do Docker)
|
||||||
|
make install # pnpm install + uv sync
|
||||||
|
make dev-web # vite na 5173
|
||||||
|
make dev-api # uvicorn --reload na 3000
|
||||||
|
make migrate # roda migrations
|
||||||
|
make build # build do front
|
||||||
|
make typecheck / lint
|
||||||
|
|
||||||
|
# Stack Docker (requer infra-net + platform layer de pé)
|
||||||
|
make up # up -d --build
|
||||||
|
make down / rebuild / logs / ps
|
||||||
|
make deploy # git reset --hard + up (chamado pelo CI)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Convenções
|
||||||
|
|
||||||
|
### Variáveis de ambiente
|
||||||
|
- **SCREAMING_SNAKE_CASE**
|
||||||
|
- Cada stack tem seu `.env` (cópia do `.env.example` ao lado)
|
||||||
|
- Variáveis compartilhadas entre stacks (`DOMAIN_BASE`, `MINIO_ROOT_*`, `WEDDING_DB_*`) precisam casar manualmente
|
||||||
|
- Senhas defaults nos `.env.example` são placeholders `troque-essa-senha-forte` — sempre trocar
|
||||||
|
- `SESSION_SECRET` mínimo 16 chars (sugestão: UUID + sufixo)
|
||||||
|
|
||||||
|
### IDs
|
||||||
|
- Nanoid 16 chars + prefixo: `up_xxxx...` (upload), `au_xxxx...` (audit)
|
||||||
|
- Implementação em `apps/api/app/lib/ids.py`
|
||||||
|
|
||||||
|
### API wire format
|
||||||
|
- **camelCase** (`coupleNames`, `createdAt`, `maxFileMb`, etc.)
|
||||||
|
- Mesmo schema entre Python (Pydantic) e TS (Zod) — campos batem nome-a-nome
|
||||||
|
- Erros: `{"error": "code", "details": ...}` com HTTP status apropriado
|
||||||
|
|
||||||
|
### Timestamps
|
||||||
|
- `bigint` em ms desde epoch (não `timestamptz`)
|
||||||
|
- Razões: lida fácil com JS `Date.now()`, sort sem timezone, não precisa de cast
|
||||||
|
- Default no DB: `(EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT`
|
||||||
|
|
||||||
|
### Migrations
|
||||||
|
- SQL puro em `apps/api/app/migrations/NNNN_descricao.sql`
|
||||||
|
- Runner em `app/db/migrate.py`: idempotente, valida SHA256 (impede editar migration aplicada)
|
||||||
|
- Roda automaticamente no boot do `wedding_app` (`AUTO_MIGRATE=true`)
|
||||||
|
- Schema sempre em **Postgres puro** (não SQLite syntax)
|
||||||
|
|
||||||
|
### Storage keys
|
||||||
|
- Formato: `uploads/{YYYY}/{MM}/{id}.{ext}`
|
||||||
|
- Ex.: `uploads/2026/06/up_abc123def456.jpg`
|
||||||
|
- Extensão vem do filename, fallback no MIME type
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Fluxos importantes
|
||||||
|
|
||||||
|
### Upload (convidado)
|
||||||
|
1. `POST /api/uploads/init` → backend valida (tamanho, vídeo permitido, duração), cria row `pending`, retorna URL pré-assinada
|
||||||
|
2. Browser faz **`PUT` direto no MinIO** (não passa pelo backend) com a URL pré-assinada
|
||||||
|
3. Decisão **single vs multipart**:
|
||||||
|
- `<= 50 MB`: single PUT
|
||||||
|
- `> 50 MB`: multipart 10 MB chunks
|
||||||
|
4. `POST /api/uploads/:id/confirm` → backend faz HEAD pra verificar o objeto, completa multipart se aplicável, **se for HEIC**: transcoda pra JPEG e substitui o storage_key, marca `approved` (ou `pending` se moderation=`pre`)
|
||||||
|
5. Galeria pública lista só `status=approved`
|
||||||
|
|
||||||
|
### HEIC transcoding
|
||||||
|
- Detecção: `mime_type in {"image/heic", "image/heif"}`
|
||||||
|
- Em `/confirm` após HEAD: baixa, decoda com pillow-heif, aplica EXIF rotation, salva JPEG quality 88 progressive, escreve com `.jpg`, deleta HEIC
|
||||||
|
- **Falha não bloqueia o upload**: log + mantém HEIC original (gallery mostra placeholder)
|
||||||
|
- Razão: browsers (especialmente Android) não renderizam HEIC nativamente
|
||||||
|
|
||||||
|
### Multipart upload (cliente)
|
||||||
|
- Em `apps/web/src/lib/upload.ts`
|
||||||
|
- Sequential (não paralelo pra MVP) com `XMLHttpRequest` (precisa de progress event)
|
||||||
|
- ETag de cada chunk via header `etag` na resposta — exige CORS `ExposeHeaders: ["ETag"]` no bucket
|
||||||
|
|
||||||
|
### Admin login
|
||||||
|
- `POST /api/admin/login` body `{email, password}`
|
||||||
|
- Valida email contra `ALLOWED_ADMIN_EMAILS` (separados por vírgula no env)
|
||||||
|
- Compara senha com `ADMIN_PASSWORD` via `hmac.compare_digest` (constant-time)
|
||||||
|
- Issue cookie HttpOnly JWT HS256 com email + exp 7 dias
|
||||||
|
- `GET /api/admin/*` decora com `Depends(get_admin_email)` que verifica o cookie
|
||||||
|
|
||||||
|
### Admin: gestão de uploads
|
||||||
|
- `GET /api/admin/uploads?status=...&kind=photo|video&q=...&cursor=...` — paginação por timestamp DESC
|
||||||
|
- `PATCH /api/admin/uploads/:id` — edita `authorName` + `message`
|
||||||
|
- `POST /api/admin/uploads/:id/{approve,reject,cover}` — actions individuais
|
||||||
|
- `DELETE /api/admin/uploads/:id` — apaga do banco + storage (incl. thumbnail)
|
||||||
|
- `POST /api/admin/uploads/bulk` — `{action: approve|reject|delete, ids: [...]}` até 200 IDs
|
||||||
|
- `DELETE /api/admin/event/cover` — remove a foto de capa
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
- **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.)
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
3. Cria user admin via CLI (nome **NÃO pode ser `admin`** — é reservado):
|
||||||
|
```bash
|
||||||
|
read -s PW
|
||||||
|
docker exec -u git -it gitea gitea admin user create \
|
||||||
|
--username lronetto --password "$PW" --email lronetto@gmail.com --admin
|
||||||
|
```
|
||||||
|
4. Abre `https://gitea.{DOMAIN_BASE}` → loga
|
||||||
|
5. Avatar → **Site Administration** → **Actions** → **Runners** → **Create new Runner** → copia token
|
||||||
|
6. Cola no `.env` do repo `lronetto-gitea`: `GITEA_RUNNER_TOKEN=<token>`
|
||||||
|
7. `make up-gitea` (runner agora se registra)
|
||||||
|
|
||||||
|
### Trocar senha do Gitea
|
||||||
|
```bash
|
||||||
|
read -s NEWPW
|
||||||
|
docker exec -u git -it gitea gitea admin user change-password \
|
||||||
|
--username lronetto --password "$NEWPW"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy automático (CI/CD)
|
||||||
|
Vive no repo **`lronetto-wedding`** (`.gitea/workflows/deploy.yml`). A cada push
|
||||||
|
na `main` — ou disparo manual (`workflow_dispatch`) — o runner conecta por
|
||||||
|
**SSH no host** e roda `make deploy` (git reset --hard + `up` com build no repo
|
||||||
|
wedding), seguido de health check em `/api/health`.
|
||||||
|
|
||||||
|
**Por que SSH e não docker direto no runner**: o `.env` (segredos do app) fica
|
||||||
|
só no host, fora do git. O host já tem o repo clonado; o deploy só atualiza o
|
||||||
|
código e sobe.
|
||||||
|
|
||||||
|
Setup (1ª vez):
|
||||||
|
1. No host, clone `lronetto-wedding` (ex.: `/opt/lronetto-wedding`) com um usuário
|
||||||
|
SSH que rode docker e tenha acesso ao repo. (Stack `main` precisa estar de pé.)
|
||||||
|
2. Gere um par de chaves SSH e adicione a **pública** no `~/.ssh/authorized_keys`
|
||||||
|
desse usuário.
|
||||||
|
3. No repo `lronetto-wedding` no Gitea → **Settings → Actions → Secrets**, crie:
|
||||||
|
- `DEPLOY_HOST` — IP/hostname do host
|
||||||
|
- `DEPLOY_USER` — usuário SSH
|
||||||
|
- `DEPLOY_SSH_KEY` — a chave **privada** (conteúdo completo)
|
||||||
|
- `DEPLOY_PATH` — caminho do repo no host (ex.: `/opt/lronetto-wedding`)
|
||||||
|
4. (Opcional) **Variables**: `DEPLOY_PORT` se o SSH não for 22.
|
||||||
|
|
||||||
|
`make deploy` (no repo wedding) aceita `BRANCH=` pra sobrescrever a branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Gotchas conhecidos
|
||||||
|
|
||||||
|
### Postgres init script só roda na 1ª vez
|
||||||
|
`postgres/init/01-create-databases.sh` (neste repo, `lronetto-main`) é executado pelo entrypoint do postgres **apenas quando `PGDATA` está vazio**. Pra "rerodar":
|
||||||
|
```bash
|
||||||
|
make down-main
|
||||||
|
sudo rm -rf postgres/data
|
||||||
|
make up-main
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativa: criar manualmente via `docker exec -i postgres psql`.
|
||||||
|
|
||||||
|
### `docker exec -it` com heredoc
|
||||||
|
`-t` aloca TTY e conflita com stdin redirecionado. Usar **`-i` só**:
|
||||||
|
```bash
|
||||||
|
docker exec -i postgres psql -U postgres <<EOF
|
||||||
|
CREATE ROLE gitea WITH LOGIN PASSWORD 'xxx';
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gitea reservou nomes
|
||||||
|
`admin`, `api`, `user`, `org`, `explore`, `install`, `repo` etc. são reservados. Use outro nome (ex.: `lronetto`, `gitadmin`). O role admin vem do flag `--admin`, não do username.
|
||||||
|
|
||||||
|
### Gitea: imagem regular vs rootless
|
||||||
|
Volumes diferentes:
|
||||||
|
- `gitea/gitea:1.22` (regular): `/data`
|
||||||
|
- `gitea/gitea:1.22-rootless`: `/var/lib/gitea` + `/etc/gitea`
|
||||||
|
|
||||||
|
Usamos a **regular** com `INSTALL_LOCK=true` pra pular o wizard `/install` no primeiro boot.
|
||||||
|
|
||||||
|
### `docker exec` por padrão entra como root
|
||||||
|
Pra Gitea, isso quebra (`Gitea is not supposed to be run as root`). Sempre `docker exec -u git ...` quando for chamar binário do gitea.
|
||||||
|
|
||||||
|
### CORS no MinIO
|
||||||
|
Bucket precisa de `ExposeHeaders: ["ETag"]` pra multipart funcionar (cliente lê ETag de cada PUT). Aplicado pelo `minio/init.sh` (repo `lronetto-main`) via `mc cors set`.
|
||||||
|
|
||||||
|
### Postgres path-style URLs
|
||||||
|
`S3_FORCE_PATH_STYLE=true` é necessário pra MinIO. Sem isso, boto3 monta URL virtual-hosted (`bucket.endpoint/key`) que MinIO single-instance não suporta.
|
||||||
|
|
||||||
|
### Mudança em `.env` exige restart
|
||||||
|
Compose só lê env vars no boot do container. `make down-X && make up-X` após editar.
|
||||||
|
|
||||||
|
### TLS local em `*.localhost`
|
||||||
|
Caddy emite cert auto-assinado pelo root local. Browser pede pra aceitar (1x por subdomínio). Pra evitar prompts:
|
||||||
|
```bash
|
||||||
|
docker exec caddy cat /data/caddy/pki/authorities/local/root.crt > /tmp/caddy-root.crt
|
||||||
|
# Importa no trust store do OS/browser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configurar firewall em produção
|
||||||
|
- VPS firewall (ufw/iptables): liberar 80 e 443
|
||||||
|
- **Cloud firewall** (security group AWS/DO/Vultr): também liberar 80 e 443 — esse é separado e quase sempre é o esquecido
|
||||||
|
- Caddy ACME challenge precisa de **80 acessível externamente**, senão Let's Encrypt falha
|
||||||
|
|
||||||
|
### DNS precisa apontar antes do `up`
|
||||||
|
Pra produção, registros A pros subdomínios (`wedding`, `gitea`, `media`, `pgadmin`, `minio`) precisam estar propagados antes do Caddy tentar emitir cert. Senão ele entra em backoff e demora.
|
||||||
|
|
||||||
|
### `ACME_EMAIL` obrigatório em prod
|
||||||
|
Let's Encrypt requer email pra contato de renovação. Em dev pode ficar vazio (Caddy usa internal CA).
|
||||||
|
|
||||||
|
### Hairpin DNS no `wedding_app`
|
||||||
|
O `extra_hosts: host-gateway` pra `media.{DOMAIN_BASE}` faz o container resolver o subdomínio pro host. Sem isso, requests server-side (HEAD/DELETE/multipart complete) vão pra IP público → roteador → host → Caddy (lentidão). Com host-gateway: container → host → Caddy (rápido).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Histórico de iterações (sem detalhe — referência rápida)
|
||||||
|
|
||||||
|
1. **MVP Cloudflare**: Workers + D1 + R2 + Pages + Access. Funcionou mas Access não rola em `*.workers.dev`.
|
||||||
|
2. **Migração 1**: full Docker (Node/Hono + Postgres + MinIO + Caddy). Single compose, deploy num VPS.
|
||||||
|
3. **Migração 2**: backend reescrito em Python (FastAPI + SQLAlchemy + boto3 + pyjwt). Mesmo contrato de API. Frontend não mexeu.
|
||||||
|
4. **HEIC + backup**: pillow-heif no `/confirm`, 2 sidecars de backup (Postgres + MinIO mirror).
|
||||||
|
5. **Reorganização em 3 stacks**: `infra/{main,gitea,wedding_photo}` no monorepo `wedding-app`, compartilhando `infra-net`. Caddy concentrado em `main/`.
|
||||||
|
6. **Deploy CI + rename main**: workflow Gitea Actions de deploy via SSH; branch renomeada pra `main`.
|
||||||
|
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`)
|
||||||
|
- Arquivados no monorepo `wedding-app`: `claude/wedding-qrcode-photos-C0PQt` (Cloudflare original), `claude/docker-vps-migration` (1ª migração Docker Node)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Onde olhar pra estender
|
||||||
|
|
||||||
|
| Quero adicionar... | Olha em |
|
||||||
|
|---|---|
|
||||||
|
| Nova rota pública | `apps/api/app/routes/public.py` |
|
||||||
|
| Nova rota admin | `apps/api/app/routes/admin.py` |
|
||||||
|
| Novo campo no upload | `apps/api/app/db/models.py` + nova migration + `apps/api/app/schemas/api.py` + atualizar `routes/uploads.py` |
|
||||||
|
| 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 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` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Decisões deferidas (a fazer se necessário)
|
||||||
|
|
||||||
|
- **Thumbnails server-side**: galeria carrega imagens full-size. Pra otimizar: `sharp` no upload `/confirm` (ou um job async), salvar `thumbnail_key`. ~2h.
|
||||||
|
- **Export ZIP do admin**: streamed ZIP de todos os uploads. ~1h.
|
||||||
|
- **Rate limit em `/uploads/init`**: hoje sem limite. Vale colocar Redis-based se houver suspeita de abuso. ~1h.
|
||||||
|
- **Email aos noivos quando upload chegar**: Resend ou SMTP. ~2h.
|
||||||
|
- **Slideshow pra projetar na recepção**: tela `/slideshow` com auto-advance. ~30 min.
|
||||||
|
- **Custom domain do bucket público** (em vez de `media.X`): mais "branded". DNS + CNAME pro MinIO. ~15 min.
|
||||||
|
- **Pivot multi-tenant SaaS**: ver seção 1 — 2-4 semanas se valer a pena.
|
||||||
90
Makefile
Normal file
90
Makefile
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
.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
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# Ajuste os caminhos abaixo se usar outro layout no disco.
|
||||||
|
|
||||||
|
NET := infra-net
|
||||||
|
WEDDING_DIR ?= ../lronetto-wedding
|
||||||
|
GITEA_DIR ?= ../lronetto-gitea
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Stacks: 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 " make restart down + up"
|
||||||
|
@echo " make status ps de todas as stacks"
|
||||||
|
@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 network Cria a network external '$(NET)' (idempotente)"
|
||||||
|
|
||||||
|
network:
|
||||||
|
@./network.sh
|
||||||
|
|
||||||
|
up: network up-main up-gitea up-wedding
|
||||||
|
|
||||||
|
down: down-wedding down-gitea down-main
|
||||||
|
|
||||||
|
restart: down up
|
||||||
|
|
||||||
|
status:
|
||||||
|
@$(DC_MAIN) ps || true
|
||||||
|
@$(DC_GITEA) ps || true
|
||||||
|
@$(DC_WEDDING) ps || true
|
||||||
|
|
||||||
|
up-main: network
|
||||||
|
$(DC_MAIN) up -d
|
||||||
|
|
||||||
|
up-gitea: network
|
||||||
|
$(DC_GITEA) up -d
|
||||||
|
|
||||||
|
up-wedding: network
|
||||||
|
$(DC_WEDDING) up -d --build
|
||||||
|
|
||||||
|
rebuild-wedding: network
|
||||||
|
$(DC_WEDDING) build --no-cache
|
||||||
|
$(DC_WEDDING) up -d
|
||||||
|
|
||||||
|
down-main:
|
||||||
|
$(DC_MAIN) down
|
||||||
|
|
||||||
|
down-gitea:
|
||||||
|
$(DC_GITEA) down
|
||||||
|
|
||||||
|
down-wedding:
|
||||||
|
$(DC_WEDDING) down
|
||||||
|
|
||||||
|
logs-main:
|
||||||
|
$(DC_MAIN) logs -f
|
||||||
|
|
||||||
|
logs-gitea:
|
||||||
|
$(DC_GITEA) logs -f
|
||||||
|
|
||||||
|
logs-wedding:
|
||||||
|
$(DC_WEDDING) logs -f
|
||||||
|
|
||||||
|
pull-main:
|
||||||
|
$(DC_MAIN) pull
|
||||||
|
|
||||||
|
pull-gitea:
|
||||||
|
$(DC_GITEA) pull
|
||||||
|
|
||||||
|
pull-wedding:
|
||||||
|
$(DC_WEDDING) pull
|
||||||
50
caddy/Caddyfile
Normal file
50
caddy/Caddyfile
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
email {$ACME_EMAIL}
|
||||||
|
# Em dev local (*.localhost) Caddy emite cert interno automaticamente.
|
||||||
|
# Em produção, Let's Encrypt entra pelos hostnames reais.
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- Gitea (UI + git over HTTPS) -----
|
||||||
|
gitea.{$DOMAIN_BASE} {
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy gitea:3000 {
|
||||||
|
transport http {
|
||||||
|
response_header_timeout 10m
|
||||||
|
dial_timeout 30s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- Wedding (app dos noivos) -----
|
||||||
|
wedding.{$DOMAIN_BASE} {
|
||||||
|
encode zstd gzip
|
||||||
|
request_body {
|
||||||
|
max_size 600MB
|
||||||
|
}
|
||||||
|
reverse_proxy wedding_app:3000 {
|
||||||
|
transport http {
|
||||||
|
response_header_timeout 10m
|
||||||
|
dial_timeout 30s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- pgAdmin -----
|
||||||
|
pgadmin.{$DOMAIN_BASE} {
|
||||||
|
encode gzip
|
||||||
|
reverse_proxy pgadmin:80
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- MinIO console (admin web) -----
|
||||||
|
minio.{$DOMAIN_BASE} {
|
||||||
|
encode gzip
|
||||||
|
reverse_proxy minio:9001
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- MinIO S3 API público (uploads/downloads do bucket) -----
|
||||||
|
media.{$DOMAIN_BASE} {
|
||||||
|
encode gzip
|
||||||
|
reverse_proxy minio:9000 {
|
||||||
|
header_up Host {upstream_hostport}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
docker-compose.yml
Normal file
121
docker-compose.yml
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD}
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
TZ: ${TZ:-America/Sao_Paulo}
|
||||||
|
WEDDING_DB_USER: ${WEDDING_DB_USER:-wedding}
|
||||||
|
WEDDING_DB_PASSWORD: ${WEDDING_DB_PASSWORD}
|
||||||
|
WEDDING_DB_NAME: ${WEDDING_DB_NAME:-wedding}
|
||||||
|
GITEA_DB_USER: ${GITEA_DB_USER:-gitea}
|
||||||
|
GITEA_DB_PASSWORD: ${GITEA_DB_PASSWORD}
|
||||||
|
GITEA_DB_NAME: ${GITEA_DB_NAME:-gitea}
|
||||||
|
volumes:
|
||||||
|
- ./postgres/data:/var/lib/postgresql/data
|
||||||
|
- ./postgres/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
networks:
|
||||||
|
- infra-net
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--save", "60", "1", "--appendonly", "yes"]
|
||||||
|
volumes:
|
||||||
|
- ./redis/data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- infra-net
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: minio
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- ./minio/data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mc", "ready", "local"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
networks:
|
||||||
|
- infra-net
|
||||||
|
|
||||||
|
minio-init:
|
||||||
|
image: minio/mc:latest
|
||||||
|
container_name: minio-init
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
|
WEDDING_BUCKET: ${WEDDING_BUCKET:-wedding-media}
|
||||||
|
volumes:
|
||||||
|
- ./minio/init.sh:/init.sh:ro
|
||||||
|
- ./minio/cors.json:/cors.json:ro
|
||||||
|
entrypoint: ["/bin/sh", "/init.sh"]
|
||||||
|
networks:
|
||||||
|
- infra-net
|
||||||
|
|
||||||
|
pgadmin:
|
||||||
|
image: dpage/pgadmin4:latest
|
||||||
|
container_name: pgadmin
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
|
||||||
|
PGADMIN_DISABLE_POSTFIX: "true"
|
||||||
|
PGADMIN_CONFIG_SERVER_MODE: "True"
|
||||||
|
PGADMIN_CONFIG_PROXY_X_FOR_COUNT: "1"
|
||||||
|
PGADMIN_CONFIG_PROXY_X_PROTO_COUNT: "1"
|
||||||
|
volumes:
|
||||||
|
- ./pgadmin/data:/var/lib/pgadmin
|
||||||
|
- ./pgadmin/servers.json:/pgadmin4/servers.json:ro
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- infra-net
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
container_name: caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp"
|
||||||
|
environment:
|
||||||
|
DOMAIN_BASE: ${DOMAIN_BASE:-localhost}
|
||||||
|
ACME_EMAIL: ${ACME_EMAIL:-}
|
||||||
|
WEDDING_BUCKET: ${WEDDING_BUCKET:-wedding-media}
|
||||||
|
volumes:
|
||||||
|
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- ./caddy/data:/data
|
||||||
|
- ./caddy/config:/config
|
||||||
|
networks:
|
||||||
|
- infra-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
infra-net:
|
||||||
|
external: true
|
||||||
|
name: infra-net
|
||||||
11
minio/cors.json
Normal file
11
minio/cors.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"CORSRules": [
|
||||||
|
{
|
||||||
|
"AllowedOrigins": ["*"],
|
||||||
|
"AllowedMethods": ["PUT", "GET", "HEAD", "POST", "DELETE"],
|
||||||
|
"AllowedHeaders": ["*"],
|
||||||
|
"ExposeHeaders": ["ETag"],
|
||||||
|
"MaxAgeSeconds": 3600
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
minio/init.sh
Normal file
13
minio/init.sh
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Inicializa o bucket usado pela stack wedding_photo. 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"
|
||||||
|
|
||||||
|
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)"
|
||||||
11
network.sh
Normal file
11
network.sh
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NET="${INFRA_NETWORK:-infra-net}"
|
||||||
|
|
||||||
|
if docker network inspect "$NET" >/dev/null 2>&1; then
|
||||||
|
echo "[network] '$NET' já existe"
|
||||||
|
else
|
||||||
|
docker network create --driver bridge "$NET" >/dev/null
|
||||||
|
echo "[network] '$NET' criada"
|
||||||
|
fi
|
||||||
13
pgadmin/servers.json
Normal file
13
pgadmin/servers.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"Servers": {
|
||||||
|
"1": {
|
||||||
|
"Name": "Postgres (infra)",
|
||||||
|
"Group": "Servers",
|
||||||
|
"Host": "postgres",
|
||||||
|
"Port": 5432,
|
||||||
|
"MaintenanceDB": "postgres",
|
||||||
|
"Username": "postgres",
|
||||||
|
"SSLMode": "prefer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
postgres/init/01-create-databases.sh
Normal file
17
postgres/init/01-create-databases.sh
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Cria os bancos e roles dedicados a cada app. Rodado pelo postgres entrypoint
|
||||||
|
# uma única vez, no primeiro boot. Subsequente: ignorado (init scripts só
|
||||||
|
# rodam quando PGDATA está vazio).
|
||||||
|
set -e
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
CREATE ROLE "${WEDDING_DB_USER}" WITH LOGIN PASSWORD '${WEDDING_DB_PASSWORD}';
|
||||||
|
CREATE DATABASE "${WEDDING_DB_NAME}" OWNER "${WEDDING_DB_USER}";
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE "${WEDDING_DB_NAME}" TO "${WEDDING_DB_USER}";
|
||||||
|
|
||||||
|
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}";
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
echo "[postgres-init] roles + databases criados para wedding (${WEDDING_DB_NAME}) e gitea (${GITEA_DB_NAME})"
|
||||||
Loading…
Reference in New Issue
Block a user