All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 2m3s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 1m31s
The payment page lists credit card (preselected), debit card and PIX. Each card option limits Mercado Pago's form to its kind; debit is paid at once. Card payments ask for 3-D Secure when the issuer requires it, and a challenge opens the bank's page in a frame, which needs PAYMENT_CHALLENGE_SOURCES=https: (frames and form posts only). A card left waiting for that confirmation stops blocking a new attempt after ten minutes, and a refusal reported by the notification returns the customer to the payment choice. Written from the documentation; not yet run with a real debit card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
235 lines
9.0 KiB
YAML
235 lines
9.0 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}
|
|
# fake until Mercado Pago is configured; mercadopago requires MP_ACCESS_TOKEN
|
|
# and MP_WEBHOOK_SECRET or the API and worker refuse to start. Test
|
|
# credentials (TEST-...) until the sandbox flows have passed. The card form
|
|
# appears only with MP_PUBLIC_KEY, and then needs PAYMENT_CSP_SOURCES too.
|
|
PAYMENT_ADAPTER: ${PAYMENT_ADAPTER:-fake}
|
|
MP_ACCESS_TOKEN: ${MP_ACCESS_TOKEN:-}
|
|
# The "assinatura secreta" from the webhook settings in Mercado Pago.
|
|
MP_WEBHOOK_SECRET: ${MP_WEBHOOK_SECRET:-}
|
|
# https://<SITE_DOMAIN>/api/payments/webhook, sent with every payment.
|
|
MP_NOTIFICATION_URL: ${MP_NOTIFICATION_URL:-}
|
|
MP_PUBLIC_KEY: ${MP_PUBLIC_KEY:-}
|
|
# The fake adapter's secret. Optional: without it the webhook verifies
|
|
# nothing and therefore accepts nothing, which is the correct state until a
|
|
# provider is connected. Never set it to a value anyone could guess.
|
|
PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET:-}
|
|
FREIGHT_ADAPTER: fake
|
|
# A cart the Site priced is approved at checkout and can be paid at once;
|
|
# orders above QUOTE_AUTO_MAX_METRES, or with a grade the Site did not
|
|
# compute, wait for an operator on the Kanban (app/quote_review.py).
|
|
QUOTE_AUTO_APPROVE: ${QUOTE_AUTO_APPROVE:-true}
|
|
QUOTE_AUTO_MAX_METRES: ${QUOTE_AUTO_MAX_METRES:-50}
|
|
# 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:-}
|
|
# The client's "retirar pessoalmente" forma de envio id, on pickup orders.
|
|
TINY_FORMA_ENVIO_RETIRADA: ${TINY_FORMA_ENVIO_RETIRADA:-}
|
|
# Sets "Aprovada" on paid orders and "Pronto para envio" on finished pickup
|
|
# orders, which the client's Tiny -> n8n notices send to customers. Turn on
|
|
# only together with TINY_ADAPTER=tiny and after n8n stops sending the
|
|
# designer message for DTFIMP products.
|
|
TINY_STATUS_UPDATES: ${TINY_STATUS_UPDATES:-false}
|
|
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:-}
|
|
# The bank's confirmation page for debit and other 3-D Secure cards is
|
|
# on the issuer's own domain, so it cannot be listed: "https:" lets
|
|
# frames and form posts reach it (never scripts). Empty turns it off.
|
|
PAYMENT_CHALLENGE_SOURCES: ${PAYMENT_CHALLENGE_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: ""
|
|
PAYMENT_CHALLENGE_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
|