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 2m23s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images and notify Portainer (push) Has been skipped
PDF artwork: a single-page PDF source is placed in the print file as a vector form through pikepdf, never rasterised, using the CropBox and inherited /Rotate the Site measured with pdf.js. Multi-page and protected PDFs go to hand preparation. PyMuPDF was not used because of its AGPL licence. Raster tests cover crop, page rotation, placement rotation and mirroring, and fail when the rotation or crop handling is broken. Card payment: Mercado Pago's Card Payment Brick on the Site when MP_PUBLIC_KEY is set; the card becomes a one-time token in Mercado Pago's secure fields. Each card attempt has its own idempotency key, and the intent route refuses new attempts once a payment is approved or a card is in review, so a quote cannot be charged twice. The Site CSP admits Mercado Pago's origins only through PAYMENT_CSP_SOURCES, empty by default. Logins: every attempt counts against the source address, only failures against the account. Counting successful sign-ins let ordinary use lock an operator out and made CI's final browser sign-in fail. No new required settings; production behaviour is unchanged until the provider credentials are configured. Verified with the full CI integration sequence locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
7.3 KiB
YAML
208 lines
7.3 KiB
YAML
version: "3.8"
|
|
|
|
x-app-environment: &app-environment
|
|
APP_ENV: production
|
|
DATABASE_HOST: db
|
|
DATABASE_NAME: dtf
|
|
DATABASE_USER: dtf_app
|
|
# Must differ from POSTGRES_PASSWORD: the runtime role is DML-only, and reusing
|
|
# the administrator credential would make that restriction meaningless.
|
|
DATABASE_PASSWORD: ${APP_DB_PASSWORD:?set APP_DB_PASSWORD}
|
|
S3_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT}
|
|
S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT}
|
|
S3_BUCKET: ${R2_BUCKET:?set R2_BUCKET}
|
|
AWS_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID:?set R2_ACCESS_KEY_ID}
|
|
AWS_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY:?set R2_SECRET_ACCESS_KEY}
|
|
AWS_DEFAULT_REGION: auto
|
|
# Optional at deploy time so a missing Kanban credential cannot make the
|
|
# entire Swarm stack invalid. The Kanban login endpoint fails closed until
|
|
# this value is configured.
|
|
OPERATOR_EMAIL: ${OPERATOR_EMAIL:-}
|
|
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:?set OPERATOR_PASSWORD}
|
|
PAYMENT_ADAPTER: fake
|
|
# Optional: without it the webhook verifies nothing and therefore accepts
|
|
# nothing, which is the correct state until a provider is connected. Set it
|
|
# when the provider is configured, never to a value anyone could guess.
|
|
PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET:-}
|
|
FREIGHT_ADAPTER: fake
|
|
# Order creation in Tiny stays off until it has been tested against the
|
|
# client's account (Tiny has no sandbox). The application credentials can be
|
|
# set now: they let an operator connect Tiny from the Kanban, and the worker
|
|
# keeps that connection alive. Callback: https://<KANBAN_DOMAIN>/api/operator/tiny/callback
|
|
TINY_ADAPTER: fake
|
|
TINY_CLIENT_ID: ${TINY_CLIENT_ID:-}
|
|
TINY_CLIENT_SECRET: ${TINY_CLIENT_SECRET:-}
|
|
TINY_REDIRECT_URI: ${TINY_REDIRECT_URI:-}
|
|
TINY_PRODUCT_TEXTIL_FOLHA: ${TINY_PRODUCT_TEXTIL_FOLHA:-}
|
|
TINY_PRODUCT_TEXTIL_AVULSA: ${TINY_PRODUCT_TEXTIL_AVULSA:-}
|
|
TINY_PRODUCT_UV_FOLHA: ${TINY_PRODUCT_UV_FOLHA:-}
|
|
TINY_PRODUCT_UV_AVULSA: ${TINY_PRODUCT_UV_AVULSA:-}
|
|
WHATSAPP_ADAPTER: fake
|
|
STORAGE_ADAPTER: s3-r2
|
|
PUBLIC_ORIGIN: https://${SITE_DOMAIN:?set SITE_DOMAIN}
|
|
PUBLIC_HOST: ${SITE_DOMAIN:?set SITE_DOMAIN}
|
|
ALLOWED_HOSTS: ${SITE_DOMAIN:?set SITE_DOMAIN},${KANBAN_DOMAIN:?set KANBAN_DOMAIN}
|
|
ALLOWED_ORIGINS: https://${SITE_DOMAIN:?set SITE_DOMAIN},https://${KANBAN_DOMAIN:?set KANBAN_DOMAIN}
|
|
COOKIE_SECURE: "true"
|
|
MAX_UPLOAD_BYTES: "5368709120"
|
|
UPLOAD_PART_BYTES: "8388608"
|
|
STORAGE_QUOTA_BYTES: "53687091200"
|
|
OWNER_UPLOAD_QUOTA_BYTES: "10737418240"
|
|
MAX_PENDING_UPLOADS: "10"
|
|
SCAN_MAX_BYTES: "134217728"
|
|
|
|
services:
|
|
db:
|
|
image: postgres:17-alpine
|
|
environment:
|
|
POSTGRES_DB: dtf
|
|
POSTGRES_USER: dtf_admin
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
|
|
volumes: [postgres-data:/var/lib/postgresql/data]
|
|
networks: [backend]
|
|
healthcheck:
|
|
test: [CMD-SHELL, 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 20s
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy: {condition: on-failure, delay: 10s}
|
|
|
|
db-init:
|
|
image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest}
|
|
command: python -m app.bootstrap
|
|
environment:
|
|
DATABASE_ADMIN_HOST: db
|
|
DATABASE_ADMIN_NAME: dtf
|
|
DATABASE_ADMIN_USER: dtf_admin
|
|
DATABASE_ADMIN_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
|
|
APP_DB_USER: dtf_app
|
|
APP_DB_PASSWORD: ${APP_DB_PASSWORD:?set APP_DB_PASSWORD}
|
|
# The migration job seeds the first operator account from these, so an
|
|
# existing deployment keeps its Kanban login after the accounts table
|
|
# lands. Without them there would be no account at all and login would
|
|
# fail closed with 503.
|
|
OPERATOR_EMAIL: ${OPERATOR_EMAIL:-}
|
|
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:?set OPERATOR_PASSWORD}
|
|
networks: [backend]
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy: {condition: on-failure, delay: 5s, max_attempts: 20}
|
|
|
|
scanner:
|
|
image: clamav/clamav@sha256:9cb27d7660bdf66e9878c832cb433dd8aa152cfbe16f3c2c0084c80b04ae22b4
|
|
entrypoint: [clamd, --foreground=true, --config-file=/etc/clamav/clamd.conf]
|
|
configs:
|
|
- source: clamd_config
|
|
target: /etc/clamav/clamd.conf
|
|
mode: 0444
|
|
networks: [backend]
|
|
healthcheck:
|
|
test: [CMD, clamdscan, --config-file=/etc/clamav/clamd.conf, --ping, "3"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 20
|
|
start_period: 90s
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy: {condition: on-failure, delay: 10s}
|
|
resources:
|
|
limits: {memory: 3G}
|
|
|
|
api:
|
|
image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest}
|
|
environment: *app-environment
|
|
networks: [backend, egress]
|
|
read_only: true
|
|
tmpfs: [/tmp]
|
|
healthcheck:
|
|
test: [CMD-SHELL, 'python -c "import os,urllib.request; r=urllib.request.Request(\"http://localhost:8000/health\",headers={\"Host\":os.environ[\"PUBLIC_HOST\"]}); urllib.request.urlopen(r,timeout=3)"']
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 30s
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy: {condition: on-failure, delay: 5s}
|
|
|
|
worker:
|
|
image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest}
|
|
command: python -m app.worker
|
|
environment: *app-environment
|
|
networks: [backend, egress]
|
|
read_only: true
|
|
tmpfs: [/tmp]
|
|
healthcheck:
|
|
test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://localhost:8002/health',timeout=3)"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 90s
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy: {condition: on-failure, delay: 5s}
|
|
|
|
site:
|
|
image: ${WEB_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-web}:${IMAGE_TAG:-latest}
|
|
environment:
|
|
WEB_INDEX: index.html
|
|
PUBLIC_HOST: ${SITE_DOMAIN:?set SITE_DOMAIN}
|
|
S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT}
|
|
# Mercado Pago's card form loads from these origins; empty keeps the
|
|
# Site at script-src 'self'. Set together with the Mercado Pago adapter.
|
|
PAYMENT_CSP_SOURCES: ${PAYMENT_CSP_SOURCES:-}
|
|
networks: [backend]
|
|
ports:
|
|
- target: 8080
|
|
published: ${SITE_PORT:-18080}
|
|
protocol: tcp
|
|
mode: ingress
|
|
healthcheck:
|
|
test: [CMD-SHELL, 'wget -q --header="Host: $$PUBLIC_HOST" -O /dev/null http://127.0.0.1:8080/health']
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 15s
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy: {condition: on-failure, delay: 5s}
|
|
|
|
kanban:
|
|
image: ${WEB_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-web}:${IMAGE_TAG:-latest}
|
|
environment:
|
|
WEB_INDEX: kanban.html
|
|
PUBLIC_HOST: ${KANBAN_DOMAIN:?set KANBAN_DOMAIN}
|
|
S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT}
|
|
PAYMENT_CSP_SOURCES: ""
|
|
networks: [backend]
|
|
ports:
|
|
- target: 8080
|
|
published: ${KANBAN_PORT:-18081}
|
|
protocol: tcp
|
|
mode: ingress
|
|
healthcheck:
|
|
test: [CMD-SHELL, 'wget -q --header="Host: $$PUBLIC_HOST" -O /dev/null http://127.0.0.1:8080/health']
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 15s
|
|
deploy:
|
|
replicas: 1
|
|
restart_policy: {condition: on-failure, delay: 5s}
|
|
|
|
configs:
|
|
clamd_config:
|
|
file: ./infra/clamd.conf
|
|
|
|
volumes:
|
|
postgres-data:
|
|
|
|
networks:
|
|
backend:
|
|
driver: overlay
|
|
internal: true
|
|
egress:
|
|
driver: overlay
|