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 <noreply@anthropic.com>
This commit is contained in:
@@ -12,16 +12,19 @@ public container images and Python packages; running integrations are local.
|
|||||||
From this repository directory:
|
From this repository directory:
|
||||||
|
|
||||||
```bash
|
```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`.
|
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.
|
To customize, copy `.env.example` to `.env` and edit it. Never use real credentials.
|
||||||
For a detached start with readiness verification:
|
For a detached start with readiness verification:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up --build -d --wait
|
docker compose -f compose.local.yaml up --build -d --wait
|
||||||
docker compose ps
|
docker compose -f compose.local.yaml ps
|
||||||
```
|
```
|
||||||
|
|
||||||
| Component | Local URL / port | Purpose |
|
| 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
|
Use `localhost` consistently; mixing it with `127.0.0.1` creates a different
|
||||||
browser session. Published ports bind only to `127.0.0.1`.
|
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`.
|
MinIO default login: `dtf_local` / `local-storage-only`.
|
||||||
These are public, disposable development values, not real credentials.
|
These are public, disposable development values, not real credentials.
|
||||||
Interactive API documentation is disabled; `/docs` and `/redoc` are not local
|
Interactive API documentation is disabled; `/docs` and `/redoc` are not local
|
||||||
|
|||||||
197
compose.local.yaml
Normal file
197
compose.local.yaml
Normal file
@@ -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:
|
||||||
Reference in New Issue
Block a user