mirror of https://github.com/interlegis/sapl.git
Browse Source
- nginx: sendfile on, tcp_nopush, reduced keepalive/proxy timeouts - nginx: GeoIP2 ASN-based bot blocking (cloud providers + known scrapers) - nginx: UA blocklist (GPTBot, ClaudeBot, Chrome/98.0.4758 impersonator, etc.) - nginx: rate-limit zones (30r/m general, 10r/m heavy/relatorios), 429/500 error pages - nginx: proper ETags + Cache-Control on /media/ to stop 30GB logo re-transfers - Dockerfile: install libnginx-mod-http-geoip2; download GeoLite2-ASN.mmdb via BuildKit secret (key never baked into image layers); ARG GEOIP_CACHE_BUST for forced re-download without --no-cache - Gunicorn: workers 3->2, threads 8->4, timeout 300->120, max_memory 300->400MB - Django: FILE_UPLOAD_MAX_MEMORY_SIZE=2MB, FILE_UPLOAD_TEMP_DIR for large uploads - relatorios/views.py: fix N+1 in get_etiqueta_protocolos with bulk-fetch MateriaLegislativa + DocumentoAdministrativo using select_related + dict lookups - Add robots.txt, 429.html, 500.html static pages - docker-compose.yaml: use sapl:local for local dev - docker/README.md: build instructions with MAXMIND_LICENSE_KEY - rate-limiter-v2.md: canonical planning document (Architecture through Phase 5) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>rate-limiter-2026
12 changed files with 1714 additions and 53 deletions
@ -0,0 +1,107 @@ |
|||||
|
# SAPL Docker Build |
||||
|
|
||||
|
## Building locally |
||||
|
|
||||
|
### 1. Prerequisites |
||||
|
|
||||
|
- Docker 23+ with BuildKit enabled (default since Docker 23) |
||||
|
- A free [MaxMind account](https://www.maxmind.com/en/geolite2/signup) with a license key |
||||
|
|
||||
|
### 2. Set your MaxMind license key |
||||
|
|
||||
|
Add the key to the project root `.env` file (already gitignored): |
||||
|
|
||||
|
``` |
||||
|
MAXMIND_LICENSE_KEY=your_key_here |
||||
|
``` |
||||
|
|
||||
|
The key is used **only at build time** to download the `GeoLite2-ASN.mmdb` database for |
||||
|
nginx ASN-based bot blocking. It is injected via a BuildKit secret and is **never stored |
||||
|
in any image layer** — it will not appear in `docker history` or any registry push. |
||||
|
|
||||
|
### 3. Build the image |
||||
|
|
||||
|
```bash |
||||
|
docker build \ |
||||
|
--secret id=maxmind_key,src=.env \ |
||||
|
-f docker/Dockerfile \ |
||||
|
-t sapl:local \ |
||||
|
. |
||||
|
``` |
||||
|
|
||||
|
Run from the **project root** (not from inside `docker/`), so the build context includes |
||||
|
the full source tree. |
||||
|
|
||||
|
#### Optional build args |
||||
|
|
||||
|
| Arg | Default | Description | |
||||
|
|---|---|---| |
||||
|
| `WITH_NGINX` | `1` | Include nginx in the image | |
||||
|
| `WITH_GRAPHVIZ` | `1` | Include Graphviz | |
||||
|
| `WITH_POPPLER` | `1` | Include Poppler (PDF utilities) | |
||||
|
| `WITH_PSQL_CLIENT` | `1` | Include `psql` client | |
||||
|
|
||||
|
Example — build without Graphviz: |
||||
|
|
||||
|
```bash |
||||
|
docker build \ |
||||
|
--secret id=maxmind_key,src=.env \ |
||||
|
--build-arg WITH_GRAPHVIZ=0 \ |
||||
|
-f docker/Dockerfile \ |
||||
|
-t sapl:local \ |
||||
|
. |
||||
|
``` |
||||
|
|
||||
|
### 4. If the MaxMind key is not provided |
||||
|
|
||||
|
The build will succeed but nginx will log an error on startup because |
||||
|
`/etc/nginx/geoip/GeoLite2-ASN.mmdb` will be missing. ASN-based bot blocking will |
||||
|
be inactive. All other Phase 0 mitigations (UA blocklist, rate limits, ETags) still apply. |
||||
|
|
||||
|
You can mount the database file at runtime as a workaround: |
||||
|
|
||||
|
```bash |
||||
|
docker run \ |
||||
|
-v /path/to/GeoLite2-ASN.mmdb:/etc/nginx/geoip/GeoLite2-ASN.mmdb:ro \ |
||||
|
sapl:local |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Production — Harbor |
||||
|
|
||||
|
Official images are built and pushed through **Harbor**. Before the next release, configure |
||||
|
the MaxMind license key as a build secret in the Harbor / CI pipeline: |
||||
|
|
||||
|
1. Add `MAXMIND_LICENSE_KEY` as a **masked CI/CD secret** in the Harbor build project |
||||
|
(do not put it in any Helm values file or ConfigMap). |
||||
|
2. Pass it to the build step: |
||||
|
```bash |
||||
|
docker build \ |
||||
|
--secret id=maxmind_key,env=MAXMIND_LICENSE_KEY \ |
||||
|
-f docker/Dockerfile \ |
||||
|
-t harbor.your-registry/sapl/sapl:$VERSION \ |
||||
|
. |
||||
|
``` |
||||
|
Note: `env=` variant reads the secret from an environment variable instead of a file — |
||||
|
useful in CI where `.env` files are not present. |
||||
|
3. Push as normal — the key will not be present in the pushed image. |
||||
|
|
||||
|
### Keeping GeoLite2-ASN up to date |
||||
|
|
||||
|
MaxMind updates the database every Tuesday. On production hosts, install the weekly refresh |
||||
|
cron (run as root): |
||||
|
|
||||
|
```bash |
||||
|
cat > /etc/cron.weekly/update-geoip << 'EOF' |
||||
|
#!/bin/bash |
||||
|
MAXMIND_KEY="$(kubectl get secret sapl-build-secrets -n interlegis-infra \ |
||||
|
-o jsonpath='{.data.MAXMIND_LICENSE_KEY}' | base64 -d)" |
||||
|
curl -fsSL \ |
||||
|
"https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_KEY}&suffix=tar.gz" \ |
||||
|
| tar -xz -C /tmp --wildcards '*.mmdb' |
||||
|
mv /tmp/GeoLite2-ASN_*/GeoLite2-ASN.mmdb /etc/nginx/geoip/GeoLite2-ASN.mmdb |
||||
|
nginx -s reload |
||||
|
EOF |
||||
|
chmod +x /etc/cron.weekly/update-geoip |
||||
|
``` |
||||
File diff suppressed because it is too large
@ -0,0 +1,42 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="pt-BR"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>429 – Muitas Requisições</title> |
||||
|
<style> |
||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
||||
|
body { |
||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
||||
|
background: #f5f5f5; |
||||
|
color: #333; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
min-height: 100vh; |
||||
|
} |
||||
|
.card { |
||||
|
background: #fff; |
||||
|
border-top: 4px solid #e74c3c; |
||||
|
border-radius: 4px; |
||||
|
padding: 2.5rem 3rem; |
||||
|
max-width: 480px; |
||||
|
width: 90%; |
||||
|
text-align: center; |
||||
|
box-shadow: 0 2px 8px rgba(0,0,0,.08); |
||||
|
} |
||||
|
.code { font-size: 4rem; font-weight: 700; color: #e74c3c; line-height: 1; } |
||||
|
h1 { font-size: 1.25rem; margin: .75rem 0 1rem; } |
||||
|
p { font-size: .95rem; line-height: 1.6; color: #555; } |
||||
|
.retry { margin-top: 1.5rem; font-size: .85rem; color: #888; } |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="card"> |
||||
|
<div class="code">429</div> |
||||
|
<h1>Muitas Requisições</h1> |
||||
|
<p>Você realizou muitas requisições em um curto período. Aguarde um momento e tente novamente.</p> |
||||
|
<p class="retry">Se o problema persistir, entre em contato com o suporte da sua Câmara Municipal.</p> |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,42 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="pt-BR"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>500 – Erro Interno do Servidor</title> |
||||
|
<style> |
||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
||||
|
body { |
||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
||||
|
background: #f5f5f5; |
||||
|
color: #333; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
min-height: 100vh; |
||||
|
} |
||||
|
.card { |
||||
|
background: #fff; |
||||
|
border-top: 4px solid #e67e22; |
||||
|
border-radius: 4px; |
||||
|
padding: 2.5rem 3rem; |
||||
|
max-width: 480px; |
||||
|
width: 90%; |
||||
|
text-align: center; |
||||
|
box-shadow: 0 2px 8px rgba(0,0,0,.08); |
||||
|
} |
||||
|
.code { font-size: 4rem; font-weight: 700; color: #e67e22; line-height: 1; } |
||||
|
h1 { font-size: 1.25rem; margin: .75rem 0 1rem; } |
||||
|
p { font-size: .95rem; line-height: 1.6; color: #555; } |
||||
|
.retry { margin-top: 1.5rem; font-size: .85rem; color: #888; } |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="card"> |
||||
|
<div class="code">500</div> |
||||
|
<h1>Erro Interno do Servidor</h1> |
||||
|
<p>Ocorreu um erro inesperado. Nossa equipe foi notificada e trabalhará para resolver o problema.</p> |
||||
|
<p class="retry">Tente novamente em alguns instantes. Se o problema persistir, entre em contato com o suporte da sua Câmara Municipal.</p> |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,19 @@ |
|||||
|
User-agent: GPTBot |
||||
|
Disallow: / |
||||
|
Crawl-delay: 10 |
||||
|
|
||||
|
User-agent: ClaudeBot |
||||
|
Disallow: / |
||||
|
Crawl-delay: 10 |
||||
|
|
||||
|
User-agent: meta-externalagent |
||||
|
Disallow: / |
||||
|
Crawl-delay: 10 |
||||
|
|
||||
|
User-agent: OAI-SearchBot |
||||
|
Disallow: / |
||||
|
Crawl-delay: 10 |
||||
|
|
||||
|
User-agent: * |
||||
|
Disallow: /relatorios/ |
||||
|
Crawl-delay: 10 |
||||
Loading…
Reference in new issue