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>
7.9 KiB
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— normallygitea.blyzer.com.br.REGISTRY_OWNER— normallyblyzer.PYTHON_BASE_IMAGE,NGINX_BASE_IMAGE,TRIVY_IMAGE— approved immutable@sha256:image references.
Repository secrets:
REGISTRY_USERNAMEandREGISTRY_TOKEN— package write credentials.PORTAINER_WEBHOOK— webhook generated by thedtf-cloudPortainer 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:
[
{
"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 existingdtf-cloudstack uses; it must stay at the root for this reason) - Registry: the private
gitea.blyzer.com.brregistry - 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:
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:
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.