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>
267 lines
10 KiB
YAML
267 lines
10 KiB
YAML
# Localhost development stack. Builds from source, uses MinIO, fake providers and
|
|
# disposable credentials. `docker-compose.yml` is the production/R2 stack and is
|
|
# NOT usable locally; the two are deliberately separate files.
|
|
#
|
|
# docker compose -f compose.local.yaml up --build
|
|
#
|
|
# Defaults here mirror `.env.example`; copy it to `.env` only to customise.
|
|
|
|
x-app: &app
|
|
build:
|
|
context: .
|
|
dockerfile: infra/Dockerfile
|
|
environment: &environment
|
|
APP_ENV: local
|
|
DATABASE_URL: postgresql://${APP_DB_USER:-dtf_app}:${APP_DB_PASSWORD:-local-app-database-only}@db:5432/${POSTGRES_DB:-dtf_local}
|
|
S3_ENDPOINT: http://storage:9000
|
|
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:${STORAGE_PORT:-9000}}
|
|
S3_BUCKET: ${S3_BUCKET:-dtf-local-artwork}
|
|
AWS_ACCESS_KEY_ID: ${S3_APP_USER:-dtf_app}
|
|
AWS_SECRET_ACCESS_KEY: ${S3_APP_PASSWORD:-local-app-storage-only}
|
|
AWS_DEFAULT_REGION: us-east-1
|
|
OPERATOR_EMAIL: ${OPERATOR_EMAIL:-operator@example.test}
|
|
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:-local-operator-only}
|
|
# The browser reaches the API through the Site gateway, so the published
|
|
# Site/Kanban origins must be accepted or every write is rejected 403.
|
|
PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:-http://localhost:${SITE_PORT:-8080}}
|
|
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1}
|
|
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:${SITE_PORT:-8080},http://localhost:${KANBAN_PORT:-8081},http://127.0.0.1:${SITE_PORT:-8080},http://127.0.0.1:${KANBAN_PORT:-8081}}
|
|
COOKIE_SECURE: "false"
|
|
# Sandbox testing only: set PAYMENT_ADAPTER=mercadopago with the MP_* test
|
|
# credentials, or TINY_ADAPTER=tiny with a TINY_TOKEN, in .env. Never real
|
|
# production credentials on a developer machine.
|
|
PAYMENT_ADAPTER: ${PAYMENT_ADAPTER:-fake}
|
|
PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET:-local-webhook-secret}
|
|
MP_ACCESS_TOKEN: ${MP_ACCESS_TOKEN:-}
|
|
MP_WEBHOOK_SECRET: ${MP_WEBHOOK_SECRET:-}
|
|
MP_NOTIFICATION_URL: ${MP_NOTIFICATION_URL:-}
|
|
MP_PUBLIC_KEY: ${MP_PUBLIC_KEY:-}
|
|
FREIGHT_ADAPTER: fake
|
|
# Carts the Site priced are approved at checkout; larger ones wait for review.
|
|
QUOTE_AUTO_APPROVE: ${QUOTE_AUTO_APPROVE:-true}
|
|
QUOTE_AUTO_MAX_METRES: ${QUOTE_AUTO_MAX_METRES:-50}
|
|
TINY_ADAPTER: ${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:-}
|
|
TINY_ECOMMERCE_ID: ${TINY_ECOMMERCE_ID:-}
|
|
WHATSAPP_ADAPTER: fake
|
|
STORAGE_ADAPTER: s3-local
|
|
MOCK_FREIGHT_CENTS: ${MOCK_FREIGHT_CENTS:-1500}
|
|
MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-5368709120}
|
|
UPLOAD_PART_BYTES: ${UPLOAD_PART_BYTES:-8388608}
|
|
STORAGE_QUOTA_BYTES: ${STORAGE_QUOTA_BYTES:-53687091200}
|
|
OWNER_UPLOAD_QUOTA_BYTES: ${OWNER_UPLOAD_QUOTA_BYTES:-10737418240}
|
|
MAX_PENDING_UPLOADS: ${MAX_PENDING_UPLOADS:-10}
|
|
SCAN_MAX_BYTES: ${SCAN_MAX_BYTES:-134217728}
|
|
networks: [local]
|
|
init: true
|
|
security_opt: [no-new-privileges:true]
|
|
cap_drop: [ALL]
|
|
read_only: true
|
|
tmpfs: [/tmp]
|
|
logging:
|
|
driver: json-file
|
|
options: {max-size: "10m", max-file: "3"}
|
|
|
|
services:
|
|
db:
|
|
image: postgres:17-alpine
|
|
environment:
|
|
POSTGRES_DB: ${POSTGRES_DB:-dtf_local}
|
|
POSTGRES_USER: ${POSTGRES_USER:-dtf_local}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-local-database-only}
|
|
volumes: [postgres-data:/var/lib/postgresql/data]
|
|
networks: [local]
|
|
healthcheck:
|
|
test: [CMD-SHELL, 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 30
|
|
|
|
storage:
|
|
# MinIO stopped publishing public images: since September 2026 both
|
|
# Docker Hub (minio/minio) and quay.io answer anonymous pulls with 401,
|
|
# which breaks any machine or runner without a cached copy. Chainguard's
|
|
# build still pulls anonymously, ships sh and mc (the healthcheck and
|
|
# storage-init need both) and runs as a non-root user. Pinned by digest
|
|
# because Chainguard's free tier only publishes :latest. Override
|
|
# MINIO_IMAGE to use a mirror of your own.
|
|
image: ${MINIO_IMAGE:-cgr.dev/chainguard/minio@sha256:bd014394a80898e68c149f2311fdf8d5a2c2f3bb2c33b9327ae6d02b4b065ae1}
|
|
command: server /data --console-address :9001
|
|
environment:
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-dtf_local}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-local-storage-only}
|
|
ports:
|
|
- "127.0.0.1:${STORAGE_PORT:-9000}:9000"
|
|
- "127.0.0.1:${STORAGE_CONSOLE_PORT:-9001}:9001"
|
|
volumes: [storage-data:/data]
|
|
networks: [local, edge]
|
|
healthcheck:
|
|
test: [CMD, mc, ready, local]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 30
|
|
|
|
db-init:
|
|
build:
|
|
context: .
|
|
dockerfile: infra/Dockerfile
|
|
command: python -m app.bootstrap
|
|
environment:
|
|
# Local only: the app role keeps a password distinct from the administrator.
|
|
DATABASE_ADMIN_URL: postgresql://${POSTGRES_USER:-dtf_local}:${POSTGRES_PASSWORD:-local-database-only}@db:5432/${POSTGRES_DB:-dtf_local}
|
|
APP_DB_USER: ${APP_DB_USER:-dtf_app}
|
|
APP_DB_PASSWORD: ${APP_DB_PASSWORD:-local-app-database-only}
|
|
# 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@example.test}
|
|
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:-local-operator-only}
|
|
networks: [local]
|
|
depends_on:
|
|
db: {condition: service_healthy}
|
|
restart: on-failure
|
|
|
|
storage-init:
|
|
build:
|
|
context: .
|
|
dockerfile: infra/Dockerfile.storage-init
|
|
args:
|
|
MINIO_IMAGE: ${MINIO_IMAGE:-cgr.dev/chainguard/minio@sha256:bd014394a80898e68c149f2311fdf8d5a2c2f3bb2c33b9327ae6d02b4b065ae1}
|
|
entrypoint: [/bin/sh, /init.sh]
|
|
environment:
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-dtf_local}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-local-storage-only}
|
|
S3_APP_USER: ${S3_APP_USER:-dtf_app}
|
|
S3_APP_PASSWORD: ${S3_APP_PASSWORD:-local-app-storage-only}
|
|
S3_BUCKET: ${S3_BUCKET:-dtf-local-artwork}
|
|
networks: [local]
|
|
depends_on:
|
|
storage: {condition: service_healthy}
|
|
restart: on-failure
|
|
|
|
scanner:
|
|
# Built, not bind-mounted: see local/Dockerfile.scanner.
|
|
build:
|
|
context: .
|
|
dockerfile: infra/Dockerfile.scanner
|
|
args:
|
|
CLAMAV_IMAGE: ${CLAMAV_IMAGE:-clamav/clamav@sha256:9cb27d7660bdf66e9878c832cb433dd8aa152cfbe16f3c2c0084c80b04ae22b4}
|
|
entrypoint: [clamd, --foreground=true, --config-file=/etc/clamav/clamd.conf]
|
|
networks: [local]
|
|
security_opt: [no-new-privileges:true]
|
|
healthcheck:
|
|
test: [CMD, clamdscan, --config-file=/etc/clamav/clamd.conf, --ping, "3"]
|
|
start_period: 60s
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 30
|
|
deploy:
|
|
resources:
|
|
limits: {memory: 3G}
|
|
|
|
api:
|
|
<<: *app
|
|
command: uvicorn app.app:app --host 0.0.0.0 --port 8000 --no-access-log
|
|
depends_on:
|
|
db-init: {condition: service_completed_successfully}
|
|
storage-init: {condition: service_completed_successfully}
|
|
healthcheck:
|
|
test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 30
|
|
|
|
worker:
|
|
<<: *app
|
|
command: python -m app.worker
|
|
depends_on:
|
|
api: {condition: service_healthy}
|
|
scanner: {condition: service_healthy}
|
|
healthcheck:
|
|
test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://localhost:8002/health')"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 12
|
|
|
|
site:
|
|
build:
|
|
context: .
|
|
dockerfile: infra/Dockerfile.web
|
|
environment:
|
|
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:${STORAGE_PORT:-9000}}
|
|
# Empty unless testing Mercado Pago's card form; see docs/LOCAL_SETUP.md.
|
|
PAYMENT_CSP_SOURCES: ${PAYMENT_CSP_SOURCES:-}
|
|
PAYMENT_CHALLENGE_SOURCES: ${PAYMENT_CHALLENGE_SOURCES:-}
|
|
ports:
|
|
# Published ports are host-wide even bound to loopback, so on a shared
|
|
# machine any of them can collide with something unrelated. CI overrides
|
|
# every one; see .gitea/workflows/deploy.yml.
|
|
- "127.0.0.1:${SITE_PORT:-8080}:80"
|
|
# Convenience only: the API through its own gateway. No test uses it.
|
|
- "127.0.0.1:${API_PORT:-8000}:81"
|
|
networks: [local, edge]
|
|
depends_on:
|
|
api: {condition: service_healthy}
|
|
healthcheck:
|
|
test: [CMD, wget, -q, -O, /dev/null, http://127.0.0.1/health]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 12
|
|
|
|
kanban:
|
|
build:
|
|
context: .
|
|
dockerfile: infra/Dockerfile.web
|
|
environment:
|
|
WEB_INDEX: kanban.html
|
|
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:${STORAGE_PORT:-9000}}
|
|
PAYMENT_CSP_SOURCES: ""
|
|
PAYMENT_CHALLENGE_SOURCES: ""
|
|
ports: ["127.0.0.1:${KANBAN_PORT:-8081}:80"]
|
|
networks: [local, edge]
|
|
depends_on:
|
|
api: {condition: service_healthy}
|
|
healthcheck:
|
|
test: [CMD, wget, -q, -O, /dev/null, http://127.0.0.1/health]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 12
|
|
|
|
browser-tests:
|
|
profiles: [ci]
|
|
build:
|
|
context: .
|
|
dockerfile: infra/Dockerfile.browser-tests
|
|
environment:
|
|
CHROME_BIN: /usr/bin/chromium
|
|
CHROME_NO_SANDBOX: "1"
|
|
CHROME_TRUST_TEST_ORIGINS: "1"
|
|
SITE_BROWSER_ORIGIN: http://site
|
|
KANBAN_BROWSER_ORIGIN: http://kanban
|
|
OPERATOR_EMAIL: ${OPERATOR_EMAIL:-operator@example.test}
|
|
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:-local-operator-only}
|
|
shm_size: 1gb
|
|
networks: [local]
|
|
depends_on:
|
|
site: {condition: service_healthy}
|
|
kanban: {condition: service_healthy}
|
|
storage: {condition: service_healthy}
|
|
security_opt: [no-new-privileges:true]
|
|
cap_drop: [ALL]
|
|
|
|
volumes:
|
|
postgres-data:
|
|
storage-data:
|
|
|
|
networks:
|
|
local:
|
|
internal: true
|
|
edge:
|