From fc6f708e95a3ef3a991d62f9af6f675b7f4896ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Fri, 18 Sep 2026 18:33:27 -0300 Subject: [PATCH] chore: restore a working localhost stack docker-compose.yml became the production/R2 stack, but LOCAL_SETUP.md still documented "docker compose up --build" against .env.example, which fails on missing R2_ENDPOINT, SITE_DOMAIN and KANBAN_DOMAIN, and MinIO was gone. Add compose.local.yaml: builds from source, MinIO storage, fake providers, disposable credentials, an app database password distinct from the administrator one, and published origins in ALLOWED_ORIGINS so browser writes are not rejected. Correct the documented command and the Kanban login, which listed a username the email-validated model rejects. Co-Authored-By: Claude Opus 5 --- LOCAL_SETUP.md | 11 ++- compose.local.yaml | 197 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 compose.local.yaml diff --git a/LOCAL_SETUP.md b/LOCAL_SETUP.md index e059f5f..c4307f3 100644 --- a/LOCAL_SETUP.md +++ b/LOCAL_SETUP.md @@ -12,16 +12,19 @@ public container images and Python packages; running integrations are local. From this repository directory: ```bash -docker compose up --build +docker compose -f compose.local.yaml up --build ``` +`docker-compose.yml` is the production/R2 stack and will not start locally; the +localhost stack is always `compose.local.yaml`. + No `.env` is required: Compose has the same disposable defaults as `.env.example`. To customize, copy `.env.example` to `.env` and edit it. Never use real credentials. For a detached start with readiness verification: ```bash -docker compose up --build -d --wait -docker compose ps +docker compose -f compose.local.yaml up --build -d --wait +docker compose -f compose.local.yaml ps ``` | Component | Local URL / port | Purpose | @@ -39,7 +42,7 @@ docker compose ps Use `localhost` consistently; mixing it with `127.0.0.1` creates a different browser session. Published ports bind only to `127.0.0.1`. -Kanban default login: `operator` / `local-operator-only`. +Kanban default login: `operator@example.test` / `local-operator-only`. MinIO default login: `dtf_local` / `local-storage-only`. These are public, disposable development values, not real credentials. Interactive API documentation is disabled; `/docs` and `/redoc` are not local diff --git a/compose.local.yaml b/compose.local.yaml new file mode 100644 index 0000000..8d64c46 --- /dev/null +++ b/compose.local.yaml @@ -0,0 +1,197 @@ +# 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: local/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: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: http://localhost:${SITE_PORT:-8080} + ALLOWED_HOSTS: localhost,127.0.0.1 + 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" + PAYMENT_ADAPTER: fake + FREIGHT_ADAPTER: fake + TINY_ADAPTER: fake + 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: + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + 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:9000:9000", "127.0.0.1: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: local/Dockerfile + command: python -m local.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} + networks: [local] + depends_on: + db: {condition: service_healthy} + restart: on-failure + + storage-init: + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + 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} + volumes: + - ./local/storage-init.sh:/init.sh:ro + - ./local/storage-policy.json:/policy.json:ro + - ./local/storage-lifecycle.json:/lifecycle.json:ro + networks: [local] + depends_on: + storage: {condition: service_healthy} + restart: on-failure + + scanner: + image: clamav/clamav@sha256:9cb27d7660bdf66e9878c832cb433dd8aa152cfbe16f3c2c0084c80b04ae22b4 + entrypoint: [clamd, --foreground=true, --config-file=/etc/clamav/clamd.conf] + volumes: [./local/clamd.conf:/etc/clamav/clamd.conf:ro] + 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 local.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 local.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: local/Dockerfile.web + environment: + S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:9000} + ports: + - "127.0.0.1:${SITE_PORT:-8080}:80" + - "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: local/Dockerfile.web + environment: + WEB_INDEX: kanban.html + S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:9000} + 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 + +volumes: + postgres-data: + storage-data: + +networks: + local: + internal: true + edge: