Skip to content

Commit a105b41

Browse files
committed
docs: Adding deploy aaPanel doc
1 parent 46375e8 commit a105b41

2 files changed

Lines changed: 332 additions & 1 deletion

File tree

docs/wiki/21-deploy-aapanel.md

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
# Deploy — aaPanel (VPS com painel)
2+
3+
[← Índice](README.md)
4+
5+
Runbook completo para publicar o KernelBot num servidor com **aaPanel** (Linux). Cobre dois caminhos:
6+
7+
- **Caminho A — Python nativo** (recomendado): venv + Uvicorn gerido pelo aaPanel, Nginx como proxy reverso.
8+
- **Caminho B — Docker**: usa o `Dockerfile`/`docker-compose.yml` do repo via gestor Docker do aaPanel.
9+
10+
Variáveis detalhadas: [12-configuracao.md](12-configuracao.md). Deploy Railway/Docker genérico: [20-deploy-railway.md](20-deploy-railway.md).
11+
12+
> **Analogia Laravel:** o fluxo é o mesmo de publicar um app Laravel num VPS — Nginx na frente, um processo de aplicação atrás (aqui Uvicorn no lugar do PHP-FPM), `.env` com credenciais, MySQL do painel e um "seeder" (ingest) para popular a base.
13+
14+
---
15+
16+
## 1. Pré-requisitos
17+
18+
| Item | Detalhe |
19+
|------|---------|
20+
| VPS Linux | Ubuntu 22.04+ / Debian 12 recomendado, mínimo 1 GB RAM (índice BM25 vive em memória) |
21+
| aaPanel instalado | `https://www.aapanel.com/new/download.html` — script oficial de instalação |
22+
| Domínio | Apontado (registo A) para o IP do VPS |
23+
| Python 3.11+ | Instalado via App Store do aaPanel (Python Manager) ou pacote do sistema |
24+
| MySQL 5.7+/8.0 | O do próprio aaPanel serve |
25+
| Chave LLM | **OpenRouter recomendado em servidor** (`ACL_LLM_PROVIDER=openrouter`) — o provider `cursor` exige runtime local do Cursor, raramente adequado em VPS |
26+
| Conteúdo | `jsons/` do repo (aulas espelhadas) e, para o catálogo, `lessons.json` + `search-index.json` do ISS |
27+
28+
Portas: o aaPanel usa 80/443 (Nginx) e a porta do painel (padrão 7800/8888 conforme instalação). A app escuta **8001 apenas em localhost** — não precisa abrir 8001 no firewall.
29+
30+
---
31+
32+
## 2. MySQL — criar base e schema
33+
34+
1. No aaPanel: **Databases → Add database**.
35+
- Nome: `kernelbot` (ou outro — vai para `DB_NAME`)
36+
- Utilizador/senha: anote — vão para `DB_USER` / `DB_PASSWORD`
37+
- Access permission: `localhost` (a app roda no mesmo servidor)
38+
2. Abra o **phpMyAdmin** (ou terminal `mysql`) e crie a tabela `knowledge` na base criada:
39+
40+
```sql
41+
CREATE TABLE IF NOT EXISTS knowledge (
42+
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
43+
discipline VARCHAR(128) NOT NULL,
44+
slug VARCHAR(255) NOT NULL,
45+
title VARCHAR(512) NOT NULL,
46+
`order` INT NOT NULL DEFAULT 0,
47+
content LONGTEXT,
48+
active TINYINT(1) NOT NULL DEFAULT 1,
49+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
50+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
51+
PRIMARY KEY (id),
52+
UNIQUE KEY uk_discipline_slug (discipline, slug),
53+
KEY idx_active (active)
54+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
55+
```
56+
57+
> É o mesmo schema de [`docker/init-knowledge.sql`](../../docker/init-knowledge.sql) (lá a base chama-se `kernelbot_staging`; em produção use o nome que criou). Garanta `utf8mb4` — conteúdo das aulas tem acentuação e símbolos.
58+
59+
---
60+
61+
## 3. Caminho A — Python nativo (recomendado)
62+
63+
### 3.1 Obter o código
64+
65+
No terminal do aaPanel (**Terminal** no menu, ou SSH):
66+
67+
```bash
68+
cd /www/wwwroot
69+
git clone https://github.com/GaabDevWeb/KernelBot.git kernelbot
70+
cd kernelbot
71+
```
72+
73+
> `/www/wwwroot` é a raiz de sites do aaPanel (equivalente ao `/var/www` clássico). Usar git facilita atualizações (`git pull`) e rollback (`git checkout <tag>`).
74+
75+
### 3.2 Ambiente virtual e dependências
76+
77+
```bash
78+
cd /www/wwwroot/kernelbot
79+
python3 -m venv .venv
80+
.venv/bin/pip install --upgrade pip
81+
.venv/bin/pip install -r requirements-prod.txt
82+
```
83+
84+
Use **`requirements-prod.txt`** (sem pytest/playwright/watchdog — mais leve, igual à imagem Docker).
85+
86+
**Frontend não precisa de build**: o CSS Tailwind compilado (`frontend/assets/css/output.css`) já está versionado no repo. Node.js só é necessário se for alterar estilos.
87+
88+
### 3.3 Configurar `.env`
89+
90+
```bash
91+
cp .env.example .env
92+
nano .env
93+
```
94+
95+
Valores obrigatórios de produção (mesma tabela do [README](../../README.md#deploy-e-produção)):
96+
97+
```bash
98+
# Ambiente
99+
KERNELBOT_ENV=production
100+
KERNELBOT_FORCE_HSTS=true # atrás do HTTPS do Nginx
101+
102+
# LLM
103+
ACL_LLM_PROVIDER=openrouter
104+
OPENROUTER_API_KEY=sk-or-...
105+
106+
# MySQL (criado no passo 2 — localhost, mesmo servidor)
107+
DB_HOST=127.0.0.1
108+
DB_PORT=3306
109+
DB_NAME=kernelbot
110+
DB_USER=kernelbot
111+
DB_PASSWORD=...
112+
113+
# Segurança operacional (obrigatório em produção)
114+
ACL_RELOAD_BEARER_TOKEN=um-token-longo-e-aleatorio # ex.: openssl rand -hex 32
115+
116+
# Catálogo ISS
117+
ACL_CATALOG_ENABLED=true
118+
ACL_CATALOG_JSON_DIR=/www/wwwroot/kernelbot-content/ISS/content
119+
```
120+
121+
Para o catálogo, copie `lessons.json` e `search-index.json` do ISS para o diretório apontado por `ACL_CATALOG_JSON_DIR` (crie-o se não existir). Sem catálogo, deixe `ACL_CATALOG_ENABLED=false``/api/curriculum` responderá 503 e o frontend esconde o mapa curricular (comportamento esperado, sem erro).
122+
123+
### 3.4 Ingestão do conteúdo (popular a tabela)
124+
125+
> **Analogia Laravel:** este passo é o `php artisan db:seed` do projeto — sem ele o chat responde "contexto insuficiente" para tudo.
126+
127+
Com o `.env` já apontando para o MySQL de produção:
128+
129+
```bash
130+
cd /www/wwwroot/kernelbot
131+
./bin/ingest-jsons.sh
132+
# ou diretamente: .venv/bin/python -m engine.jsons_ingest
133+
```
134+
135+
Isso faz UPSERT de `jsons/<disciplina>/*.json` na tabela `knowledge`. Confirme:
136+
137+
```bash
138+
mysql -u kernelbot -p kernelbot -e "SELECT discipline, COUNT(*) FROM knowledge WHERE active=1 GROUP BY discipline;"
139+
```
140+
141+
### 3.5 Teste manual antes de daemonizar
142+
143+
```bash
144+
cd /www/wwwroot/kernelbot
145+
.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8001
146+
# noutro terminal:
147+
curl -sS http://127.0.0.1:8001/health
148+
```
149+
150+
Se `/health` responder, pare (`Ctrl+C`) e passe ao processo permanente.
151+
152+
**Importante — 1 worker apenas.** Não use `--workers N`: o índice BM25 e o pin de sessão (`PinnedSessionStore`) vivem em memória do processo; múltiplos workers multiplicam RAM e quebram a consistência do pin entre requisições.
153+
154+
### 3.6 Manter o processo vivo (Supervisor do aaPanel)
155+
156+
O aaPanel tem o plugin **Supervisor Manager** (App Store → Supervisor → Install). É o equivalente ao Supervisor que mantém `queue:work` vivo no Laravel.
157+
158+
**App Store → Supervisor Manager → Add Daemon:**
159+
160+
| Campo | Valor |
161+
|-------|-------|
162+
| Name | `kernelbot` |
163+
| Run user | `www` |
164+
| Run dir | `/www/wwwroot/kernelbot` |
165+
| Start command | `/www/wwwroot/kernelbot/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8001` |
166+
| Processes | `1` |
167+
168+
Antes de iniciar, dê permissão ao utilizador `www`:
169+
170+
```bash
171+
chown -R www:www /www/wwwroot/kernelbot
172+
```
173+
174+
Alternativa sem plugin — **systemd** (via SSH):
175+
176+
```bash
177+
cat > /etc/systemd/system/kernelbot.service <<'EOF'
178+
[Unit]
179+
Description=KernelBot (FastAPI/Uvicorn)
180+
After=network.target mysql.service
181+
182+
[Service]
183+
User=www
184+
Group=www
185+
WorkingDirectory=/www/wwwroot/kernelbot
186+
ExecStart=/www/wwwroot/kernelbot/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8001
187+
Restart=always
188+
RestartSec=5
189+
190+
[Install]
191+
WantedBy=multi-user.target
192+
EOF
193+
194+
systemctl daemon-reload
195+
systemctl enable --now kernelbot
196+
systemctl status kernelbot
197+
```
198+
199+
> Nota: alguns aaPanel trazem "Python Manager"/"Python Project" na App Store que cria venv + daemon + site numa tela só. Se a sua versão tiver, pode usá-lo no lugar dos passos 3.2/3.6 — aponte o startup para `uvicorn main:app --host 127.0.0.1 --port 8001` e confira que o venv usa Python 3.11+.
200+
201+
### 3.7 Site + proxy reverso no Nginx
202+
203+
1. **Website → Add site**:
204+
- Domain: `kernelbot.seudominio.com`
205+
- PHP version: **Pure static** (não é PHP)
206+
2. Entre no site criado → **Reverse Proxy → Add reverse proxy**:
207+
- Target URL: `http://127.0.0.1:8001`
208+
- Send domain: `$host`
209+
3. **Edite a config do proxy** (Website → site → Config, ou o ficheiro do proxy em `/www/server/panel/vhost/nginx/proxy/<site>/`). O bloco `location /` precisa de suporte a **SSE** — sem `proxy_buffering off` o streaming do chat chega "de uma vez só" no fim, em vez de token a token:
210+
211+
```nginx
212+
location / {
213+
proxy_pass http://127.0.0.1:8001;
214+
proxy_http_version 1.1;
215+
proxy_set_header Host $host;
216+
proxy_set_header X-Real-IP $remote_addr;
217+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
218+
proxy_set_header X-Forwarded-Proto $scheme;
219+
220+
# SSE (POST /chat com stream) — essencial:
221+
proxy_buffering off;
222+
proxy_cache off;
223+
proxy_read_timeout 3600s;
224+
proxy_set_header Connection "";
225+
}
226+
```
227+
228+
4. Desative cache do proxy se o template do aaPanel tiver criado bloco de cache (`proxy_cache_valid` etc.) — remove ou comenta.
229+
5. Recarregue o Nginx (o painel faz isso ao salvar).
230+
231+
> O `X-Forwarded-For` importa: o rate limit de `POST /chat` (30 req/min) é por IP; sem o header, todos os utilizadores partilhariam o IP do proxy.
232+
233+
### 3.8 HTTPS (Let's Encrypt)
234+
235+
Website → site → **SSL → Let's Encrypt** → selecionar domínio → Apply. Ative **Force HTTPS**. Com o certificado ativo, mantenha `KERNELBOT_FORCE_HSTS=true` no `.env`.
236+
237+
---
238+
239+
## 4. Caminho B — Docker no aaPanel
240+
241+
Se preferir container (mesma imagem do deploy Railway):
242+
243+
1. App Store → **Docker** (instala Docker + Compose e a UI de gestão).
244+
2. Via terminal:
245+
246+
```bash
247+
cd /www/wwwroot
248+
git clone https://github.com/GaabDevWeb/KernelBot.git kernelbot
249+
cd kernelbot
250+
cp .env.docker.example .env
251+
nano .env # MySQL + LLM + ACL_RELOAD_BEARER_TOKEN (ver passo 3.3)
252+
docker compose up -d --build
253+
curl -sS http://127.0.0.1:8001/health
254+
```
255+
256+
3. MySQL: o container acessa o MySQL do aaPanel via `DB_HOST=host.docker.internal` (o compose já define `extra_hosts`). No aaPanel, o utilizador MySQL precisa aceitar conexões desse host — em **Databases**, mude a permissão do utilizador de `localhost` para `%` (ou para o IP da bridge Docker, mais restrito).
257+
4. Ingestão: rode o ingest a partir do host (passo 3.4) ou de qualquer máquina com acesso ao MySQL.
258+
5. Proxy reverso + SSL: igual aos passos 3.7 e 3.8 (o alvo continua `http://127.0.0.1:8001`, porta publicada pelo compose — configurável com `KERNELBOT_PUBLISH_PORT`).
259+
260+
---
261+
262+
## 5. Verificação pós-deploy
263+
264+
```bash
265+
# Saúde básica
266+
curl -sS https://kernelbot.seudominio.com/health
267+
268+
# Config pública — deve trazer "catalog_enabled": true (se catálogo ativo)
269+
curl -sS https://kernelbot.seudominio.com/api/public-config
270+
271+
# Catálogo — 200 em produção com ACL_CATALOG_ENABLED=true
272+
curl -sS -o /dev/null -w "%{http_code}\n" https://kernelbot.seudominio.com/api/curriculum
273+
274+
# Drift catálogo ↔ índice (usa o token do .env)
275+
curl -sS -H "Authorization: Bearer SEU_TOKEN" https://kernelbot.seudominio.com/health/catalog
276+
```
277+
278+
Teste de SSE: abra o site, envie uma pergunta no chat e confirme que a resposta chega **progressivamente** (streaming). Se vier tudo de uma vez, revise `proxy_buffering off` (passo 3.7).
279+
280+
Smoke UI opcional a partir da sua máquina local:
281+
282+
```bash
283+
SMOKE_BASE_URL=https://kernelbot.seudominio.com python3 bin/validate-frontend.py
284+
```
285+
286+
---
287+
288+
## 6. Operação
289+
290+
| Ação | Como |
291+
|------|------|
292+
| Ver logs | Supervisor Manager → kernelbot → Log; ou `journalctl -u kernelbot -f` (systemd) |
293+
| Reiniciar app | Supervisor/systemd restart — necessário após editar `.env` |
294+
| Atualizar código | `cd /www/wwwroot/kernelbot && git pull && .venv/bin/pip install -r requirements-prod.txt` → restart |
295+
| Recarregar índice sem restart | `POST /chat` com `{"message": "/reload"}` + header `Authorization: Bearer SEU_TOKEN` (após novo ingest) |
296+
| Novo conteúdo | Atualizar `jsons/` (git pull ou pipeline ISS) → `./bin/ingest-jsons.sh``/reload` |
297+
| Logs em JSON | `ACL_LOG_FORMAT=json` no `.env` (uma linha por evento — bom para grep/agregadores) |
298+
299+
### Rollback
300+
301+
1. `cd /www/wwwroot/kernelbot && git log --oneline``git checkout <commit-anterior>` (Docker: rebuild da tag anterior).
302+
2. Restart do daemon e conferir `GET /health` + `/api/public-config`.
303+
3. Se o problema for de dados: re-correr ingest + `/reload`.
304+
305+
---
306+
307+
## 7. Troubleshooting
308+
309+
| Sintoma | Causa provável | Correção |
310+
|---------|----------------|----------|
311+
| `502 Bad Gateway` | Uvicorn parado ou porta errada | Ver status no Supervisor/systemd; conferir `127.0.0.1:8001` |
312+
| Boot lento / healthcheck falha no arranque | Índice BM25 constrói no boot (pode levar dezenas de segundos com muitas aulas) | Aumentar `start_period`/tolerância do healthcheck; não é erro |
313+
| Chat responde de uma vez, sem streaming | Buffering do Nginx | `proxy_buffering off` no bloco do proxy (passo 3.7) |
314+
| Todas as respostas: "contexto insuficiente" | Tabela `knowledge` vazia ou `DB_*` errado | Rodar ingest (3.4); conferir credenciais e `SELECT COUNT(*)` |
315+
| `GET /api/curriculum` → 503 | `ACL_CATALOG_ENABLED=false` ou `ACL_CATALOG_JSON_DIR` inválido | Conferir `.env` e existência de `lessons.json`/`search-index.json`; restart |
316+
| `GET /health/catalog` → 503 `reload token not configured` | `ACL_RELOAD_BEARER_TOKEN` vazio | Definir token no `.env`; obrigatório em produção |
317+
| HTTP 429 no `/chat` | Rate limit: 30 req/min por IP (fixo em `api/routes.py`) | Esperado; se todos os IPs parecem um só, conferir `X-Forwarded-For` no Nginx |
318+
| Timeout ao conectar MySQL | `DB_HOST=127.0.0.0` (typo comum) ou permissão do utilizador | Usar `127.0.0.1`; em Docker, permissão `%`/bridge para o utilizador |
319+
| `Permission denied` no arranque | Ficheiros pertencem a root | `chown -R www:www /www/wwwroot/kernelbot` |
320+
| Erro TLS/`cryptography` no MySQL | Faltou dependência | `cryptography` está no `requirements-prod.txt` — reinstalar deps no venv |
321+
322+
---
323+
324+
## Ver também
325+
326+
- [20-deploy-railway.md](20-deploy-railway.md) — Railway, Docker genérico, rollback
327+
- [12-configuracao.md](12-configuracao.md) — todas as variáveis `.env`
328+
- [04-dados-e-mysql.md](04-dados-e-mysql.md) — contrato da tabela `knowledge`
329+
- [10-integracao-iss-fase5b.md](10-integracao-iss-fase5b.md) — pipeline de conteúdo ISS
330+
- [14-seguranca-observabilidade.md](14-seguranca-observabilidade.md) — segurança e logs

docs/wiki/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Para **alunos**, **curiosos** e **primeiro contacto** — linguagem directa, sem
3939
| 15 | [Glossário](15-glossario.md) | Termos do domínio |
4040
| 16 | [Prompts — referência](17-prompts-referencia.md) | Arquitetura ACL, inventário CL4R1T4S, padrões |
4141
| 17 | [Deploy Railway](20-deploy-railway.md) | Runbook: Docker, MySQL, variáveis, ingest, smoke, rollback |
42+
| 18 | [Deploy aaPanel](21-deploy-aapanel.md) | Runbook VPS com aaPanel: Python nativo ou Docker, Nginx/SSE, MySQL, SSL |
4243

4344
---
4445

@@ -59,6 +60,6 @@ Para **alunos**, **curiosos** e **primeiro contacto** — linguagem directa, sem
5960
| **Aluno / curioso** | [00](00-inicio-publico.md)[19](19-faq-usuario.md) |
6061
| **Novo contribuidor** | [00](00-inicio-publico.md)[18](18-contribuir.md) → 1 → 2 → 3 → 9 → 7 |
6162
| **Novo no projeto (técnico)** | 1 → 2 → 3 → 9 → 7 |
62-
| **Operador / deploy** | 20 → 10 → 13 → 12 |
63+
| **Operador / deploy** | 20 → 21 → 10 → 13 → 12 |
6364
| **RAG / retrieval** | 5 → 6 → 17 → 11 |
6465
| **Debug advisory / pin** | 6 → 8 → 13 |

0 commit comments

Comments
 (0)