All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m15s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m41s
The pickup notice under the delivery options and the invoice and retention note under the purchase summary are gone. PORTAINER.md records the R2 CORS policy the browser's direct uploads need: without it the preflight is refused and checkout fails with a NetworkError before payment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
185 lines
7.9 KiB
Markdown
185 lines
7.9 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 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. When all of them pass on a push to `main`, the images are built, scanned
|
|
and published as both `latest` and the full commit SHA. Nothing is deployed:
|
|
production changes when someone pulls and redeploys the stack in Portainer. A
|
|
manual workflow run on `main` does the same and also calls the Portainer
|
|
webhook, if `PORTAINER_WEBHOOK` is configured.
|
|
|
|
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 | yes; Chrome runs in the Compose test container |
|
|
| Trivy secret scan (HIGH/CRITICAL) | yes |
|
|
| Source preflight (`deploy/production_preflight.py --source-only`) | advisory unless `ENFORCE_PRODUCTION_PREFLIGHT` is `true`, which blocks publishing |
|
|
| Trivy image vulnerabilities, CRITICAL | yes |
|
|
| Trivy image vulnerabilities, HIGH | no — reported before publication |
|
|
| Configured Portainer webhook | no — called on manual runs when present; otherwise redeploy in Portainer |
|
|
|
|
The source preflight reports while the payment and messaging adapters are
|
|
fake. From 2026-09-23 it was enforced on every manual release, and since the
|
|
adapters are still fake no release could succeed: production kept running
|
|
older images while `main` moved on. It is now advisory everywhere. Set the
|
|
repository variable `ENFORCE_PRODUCTION_PREFLIGHT` to `true` once the real
|
|
adapters are in place, and a blocked preflight then stops images from being
|
|
published. Before a manual release, run the full configuration
|
|
preflight below against the actual Portainer values; CI checks source only.
|
|
|
|
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 release gates above pass.
|
|
`ENFORCE_PRODUCTION_PREFLIGHT` and `TRIVY_IMAGE` are optional repository
|
|
variables; the former affects push checks and the latter defaults to a pinned
|
|
scanner image.
|
|
|
|
## 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.
|
|
|
|
**R2 CORS.** The Site uploads artwork straight from the browser to the bucket
|
|
with presigned `PUT` requests, so the bucket must allow the Site's origin.
|
|
Without it the preflight is refused (403) and the Site shows "NetworkError
|
|
when attempting to fetch resource" at checkout (found 2026-09-28). In the
|
|
Cloudflare dashboard, R2 → the bucket → Settings → CORS policy:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"AllowedOrigins": ["https://<SITE_DOMAIN>", "https://<KANBAN_DOMAIN>"],
|
|
"AllowedMethods": ["PUT", "GET", "HEAD"],
|
|
"AllowedHeaders": ["content-type"],
|
|
"ExposeHeaders": ["ETag"],
|
|
"MaxAgeSeconds": 3600
|
|
}
|
|
]
|
|
```
|
|
|
|
## 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. Manual deployment
|
|
|
|
Push the reviewed commit to `main` and wait for its validation workflow to pass.
|
|
After validating the actual Portainer configuration with the full preflight in
|
|
section 3, use Gitea Actions to manually run **Build and deploy** on `main` at
|
|
that commit. The workflow repeats validation, tests and scans, then 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.
|