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 1m17s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images and notify Portainer (push) Successful in 1m37s
One OPERATOR_EMAIL and OPERATOR_PASSWORD served the whole factory, so every card movement recorded the same name and the movement history could not answer who did what. Traceability was one of the things the project set out to provide. Accounts live in dtf_local.operators, authenticated with the same scrypt hashing as customer accounts and with comparable work whether or not the account exists, so absence is not observable by timing. Administration is a CLI in the API container, like the schema migration: list, add, password, disable, enable. Passwords are read from the terminal rather than an argument so they stay out of shell history and the process list, and disabling deletes that operator's open sessions instead of leaving them valid for the rest of the eight-hour window. Migration is the part that could hurt: an empty table means 503 and a factory locked out of its Kanban. OPERATOR_EMAIL and OPERATOR_PASSWORD seed the first account, and only when that email is absent, so a password changed through the CLI survives a redeploy carrying a stale environment variable. The first attempt at this silently did nothing, because db-init receives its own small environment and had neither variable; both compose files now pass them to it. Verified against a running stack: bootstrap seeds the existing credential, that credential still logs in unchanged, a second operator authenticates separately, wrong passwords and unknown accounts are rejected alike, and disabling revokes an open session immediately. Roles are left out on purpose. The separation of duties the meeting described governs rework authorisation, which this system does not implement, so a role model would have no consumer to serve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
6.2 KiB
YAML
189 lines
6.2 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
|
|
FREIGHT_ADAPTER: fake
|
|
TINY_ADAPTER: fake
|
|
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 local.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 local.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}
|
|
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}
|
|
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: ./local/clamd.conf
|
|
|
|
volumes:
|
|
postgres-data:
|
|
|
|
networks:
|
|
backend:
|
|
driver: overlay
|
|
internal: true
|
|
egress:
|
|
driver: overlay
|