All checks were successful
Build and deploy / Validate source (push) Successful in 1m28s
Build and deploy / Integration suite on a real stack (push) Successful in 4m3s
Build and deploy / Secret scan and release gate (push) Successful in 11s
Build and deploy / Publish images and notify Portainer (push) Has been skipped
163 lines
7.0 KiB
Markdown
163 lines
7.0 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. A push to `main` does not publish or deploy. A manual workflow run on
|
|
`main` repeats those checks, builds and scans the images, then publishes both
|
|
`latest` and the full commit SHA 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 | yes; Chrome runs in the Compose test container |
|
|
| Trivy secret scan (HIGH/CRITICAL) | yes |
|
|
| Source preflight (`deploy/production_preflight.py --source-only`) | yes for manual release; advisory on pushes unless `ENFORCE_PRODUCTION_PREFLIGHT` is `true` |
|
|
| Trivy image vulnerabilities, CRITICAL | yes |
|
|
| Trivy image vulnerabilities, HIGH | no — reported before publication |
|
|
| Configured Portainer webhook | yes for manual release |
|
|
|
|
The source preflight refuses a release while the payment and messaging adapters
|
|
are fake. It remains advisory on push checks so development can continue, but a
|
|
manual release is blocked until those adapters are replaced. Set the repository
|
|
variable `ENFORCE_PRODUCTION_PREFLIGHT` to `true` when all pushes should also
|
|
fail on those blockers. 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.
|
|
|
|
## 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.
|