docs: move the engineering documents into docs/
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 1m18s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images and notify Portainer (push) Successful in 1m26s

Thirteen files at the repository root, seven of them documents. Only README.md
earns a place there; the rest are now in docs/ beside the meeting notes, the
client roadmap and the historical material.

The compose files stay. docker-compose.yml is the path the dtf-cloud Portainer
stack reads, so moving it would break deployment, and Docker resolves a compose
file's relative build contexts against its own directory, so moving the other
two would silently break every build. Both reasons are now written down where
someone would otherwise try it.

Correcting references turned up a live fault: the Portainer stack creation
instructions still named deploy/stack.yaml as the compose path. That file was
removed, so anyone recreating the stack from these instructions would have
failed. It names docker-compose.yml now, with the reason it stays at the root.

ROADMAP.md keeps the paths its closed findings were written with, and says so at
the top. Those entries record where a fault was when it was found; rewriting
them to match a later layout would make the record less true, not more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-21 17:52:20 -03:00
parent b329f76378
commit 7386469404
11 changed files with 26 additions and 21 deletions

156
docs/PORTAINER.md Normal file
View File

@@ -0,0 +1,156 @@
# Portainer deployment
DTF follows the same operating model as Graphs and ComporHUB: Gitea builds
prebuilt images, pushes them to the Gitea registry, and calls one Portainer
webhook. Portainer owns and redeploys one Docker Swarm stack named `dtf-cloud`.
The deployed stack is the repository's `docker-compose.yml`, which the
`dtf-cloud` Portainer stack points at. It was once accompanied by `deploy/stack.yaml`, a more hardened definition
supplying credentials as Docker secrets; that file was removed in favour of one
definition. See `ROADMAP.md` 2.12 for the reasoning and what remains open.
The stack contains Site, Kanban, API,
worker, PostgreSQL, ClamAV, and a one-time database initializer. Production uses
Cloudflare R2, so MinIO is not part of this stack.
## 1. Gitea configuration
The single workflow is `.gitea/workflows/deploy.yml`. Every push and pull request
runs static validation, the integration suite against a real stack, and a secret
scan. A push to `main` then builds the production images, publishes both `latest`
and the full commit SHA, reports their vulnerabilities, and calls Portainer.
What actually gates a deployment:
| Check | Gates? |
|---|---|
| `py_compile` and the unit tests | yes |
| Integration suite on a live stack (smoke, workflow, security, scanning, retention, runtime) | yes |
| Browser suites | only when the runner has Chrome; otherwise warns and continues |
| Trivy secret scan (HIGH/CRITICAL) | yes |
| Source preflight (`deploy/production_preflight.py --source-only`) | only when `ENFORCE_PRODUCTION_PREFLIGHT` is `true` |
| Trivy image vulnerabilities, CRITICAL | yes |
| Trivy image vulnerabilities, HIGH | no — reported after the build |
The source preflight is advisory by default because it refuses a release while
the payment and messaging adapters are fake, which is the deliberate state the
stack runs in today. Enforcing it now would block every deployment. Set the
repository variable `ENFORCE_PRODUCTION_PREFLIGHT` to `true` once real adapters
land, and it becomes a hard gate.
CRITICAL image findings block. Both images carry none: the bases are pinned by
digest, both Dockerfiles upgrade their OS packages, and the web image moved off
the 1.28 nginx line, which pins `nginx=1.28.3-r1` in `/etc/apk/world` and so
cannot be patched in place. HIGH findings are reported rather than enforced
because the remainder have no upstream fix.
`PYTHON_BASE_IMAGE` and `NGINX_BASE_IMAGE` override the digests pinned in the
Dockerfiles. Update the Dockerfile default in the same change, so the repository
still records what a build used.
Repository variables:
- `REGISTRY_HOST` — normally `gitea.blyzer.com.br`.
- `REGISTRY_OWNER` — normally `blyzer`.
- `PYTHON_BASE_IMAGE`, `NGINX_BASE_IMAGE`, `TRIVY_IMAGE` — approved immutable
`@sha256:` image references.
Repository secrets:
- `REGISTRY_USERNAME` and `REGISTRY_TOKEN` — package write credentials.
- `PORTAINER_WEBHOOK` — webhook generated by the `dtf-cloud` Portainer stack.
The webhook is called only after the gating checks in the table above pass.
`ENFORCE_PRODUCTION_PREFLIGHT` and `TRIVY_IMAGE` are optional repository
variables; without them the preflight is advisory and a pinned default scanner
image is used.
## 2. One-time Portainer resources
Use a Docker Swarm environment. Create a dedicated production PostgreSQL volume
and set its name as `POSTGRES_VOLUME`. Label its Swarm node
`dtf_database=true`. Do not reuse the localhost Compose volume.
Create each application credential under **Secrets**. Use versioned names such
as `dtf_prod_database_url_v1`, then place only those names in the corresponding
`*_SECRET` stack variables. The stack expects distinct secrets for:
- runtime and administrator database URLs;
- database administrator and application-role passwords;
- R2 access key and secret key;
- operator password;
- Mercado Pago token and webhook secret;
- Tiny/Olist token;
- WhatsApp token.
Credential values must never be entered into Git, `portainer.env`, or Gitea
workflow variables.
## 3. Create the stack once
In Portainer select **Stacks → Add stack → Git repository**:
- Name: `dtf-cloud`
- Repository: this Gitea repository
- Reference: `main`
- Compose path: `docker-compose.yml` (the repository default, which is what the
existing `dtf-cloud` stack uses; it must stay at the root for this reason)
- Registry: the private `gitea.blyzer.com.br` registry
- Re-pull image: enabled
- Automatic update: webhook
Load a completed copy of `deploy/portainer.env.example` as the stack's
non-secret environment variables. Do not load the example unchanged: `TBD` and
zero digests are deliberate blockers. Keep `IMAGE_TAG=latest` for normal
webhook deployments.
Before entering those values in Portainer, validate the completed ignored file:
```bash
set -a
. deploy/portainer.env
set +a
python3 deploy/production_preflight.py
```
The public reverse proxy should send the Site hostname to `SITE_PORT` and the
Kanban hostname to `KANBAN_PORT`. `/api/` and `/portal.html` are served through
the Site gateway. Preserve the original Host header. Do not expose PostgreSQL,
API, worker, or ClamAV. Restrict the
two published web ports at the host firewall so only the reverse proxy can use
them, and terminate HTTPS at the proxy.
The `db-init` service completing and stopping is expected. The other six
services must be healthy. A failed `db-init` task or an unhealthy service blocks
acceptance.
## 4. Normal deployment
Push to `main`. Gitea validates, tests, scans, publishes these images, and calls
the webhook:
```text
gitea.blyzer.com.br/blyzer/dtf-api:latest
gitea.blyzer.com.br/blyzer/dtf-api:<full-commit-sha>
gitea.blyzer.com.br/blyzer/dtf-web:latest
gitea.blyzer.com.br/blyzer/dtf-web:<full-commit-sha>
```
In Portainer, verify the new image digests, service health, `db-init` result, and
the public `/health` endpoints. Back up PostgreSQL and R2 before changes that
affect stored data or retention.
## 5. Rollback
In the Portainer stack variables, change `IMAGE_TAG` from `latest` to the full
SHA of the last known-good Gitea commit and redeploy. This rolls back Site, API,
Kanban, and worker code; it does not undo database migrations or restore data.
After recovery and a corrected release, return `IMAGE_TAG` to `latest`.
## Current blocker
This is the final deployment shape, but it is not authorized for production
today. Production adapters, Docker-secret file loading, provider sandbox tests,
current clean base images, approved inputs, and a production restore rehearsal
are still required. Do not bypass `deploy/production_preflight.py` or the Gitea
scan gates to make a deployment run.