PORTAINER.md and SECURITY_REPORT.md described a pipeline that required regressions, HIGH/CRITICAL secret, misconfiguration and image gates, and stated that the source preflight stopped this application from publishing. None of it ran: the workflow built and called the webhook unconditionally. Add a blocking Trivy secret scan. Verified both ways: a planted AWS key pair, GitHub token and private key block the job, and the repository passes clean. Note that Trivy allowlists documented example credentials, so this gate is a backstop, not permission to commit secrets. The source preflight now runs on every push and always prints its verdict, but enforces only when ENFORCE_PRODUCTION_PREFLIGHT is true. Enforcing it today would block every deployment, because it refuses a release while the payment and messaging adapters are fake, which is the deliberate state the stack runs in. Set the variable when real adapters land. Image vulnerabilities are reported after each build rather than enforced. The current bases carry 56 HIGH and 3 CRITICAL findings, only 15 of them with an upstream fix, so failing on them would stop releases without making anything safer. Pinning digests and triaging the fixable ones is ROADMAP 2.6. Both documents now carry a table of what gates and what does not, instead of describing checks that did not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
5.8 KiB
Markdown
145 lines
5.8 KiB
Markdown
# 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 production stack is `deploy/stack.yaml`. It 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 | 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.
|
|
|
|
The image scan is reported rather than enforced because the current bases carry
|
|
HIGH/CRITICAL findings with no upstream fix, so failing on them would stop
|
|
releases without making anything safer. Triage what is fixable and pin base
|
|
digests first; see `ROADMAP.md` 2.6.
|
|
|
|
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: `deploy/stack.yaml`
|
|
- 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.
|