# Combined mode (SPEC.md section 15): one VPS, SQLite, local file storage, # the job poller inline in the API process. Everything except OCR works with no # external services. Run from the repository root: docker compose up name: impuestos services: # Applies migrations and seeds the demo accounts, then exits. The API waits for it, # so `docker compose up` on a clean machine comes up ready to sign in to. migrate: build: context: .. dockerfile: apps/api/Dockerfile command: sh -c "node dist/db/migrate.cli.js && node dist/db/seed.cli.js" environment: NODE_ENV: production DATABASE_URL: sqlite:/app/data/app.db STORAGE_LOCAL_PATH: /app/data/files BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-compose-development-secret-change-me-32} BETTER_AUTH_URL: ${APP_PUBLIC_URL:-http://localhost:3000} APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000} volumes: - data:/app/data restart: 'no' api: build: context: .. dockerfile: apps/api/Dockerfile environment: NODE_ENV: production PORT: 4000 ROLE: server # SQLite is single writer, so the poller stays in this process. JOBS_INLINE: 'true' DATABASE_URL: sqlite:/app/data/app.db STORAGE_DRIVER: local STORAGE_LOCAL_PATH: /app/data/files BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-compose-development-secret-change-me-32} BETTER_AUTH_URL: ${APP_PUBLIC_URL:-http://localhost:3000} APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} DEFAULT_LOCALE: ${DEFAULT_LOCALE:-es} volumes: # One shared volume: the SQLite file and the local storage driver both live here. - data:/app/data healthcheck: test: ['CMD', 'node', '-e', "fetch('http://127.0.0.1:4000/readyz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 5s timeout: 3s retries: 12 start_period: 5s depends_on: migrate: condition: service_completed_successfully # Long enough for the 25s request drain in src/index.ts to finish. stop_grace_period: 30s restart: unless-stopped web: build: context: .. dockerfile: apps/web/Dockerfile environment: NODE_ENV: production # Cluster internal name. The browser never sees this. API_INTERNAL_URL: http://api:4000 NEXT_PUBLIC_DEFAULT_LOCALE: ${DEFAULT_LOCALE:-es} # The only port published: the browser talks to one origin and /api is proxied. ports: - '${WEB_PORT:-3000}:3000' healthcheck: test: ['CMD', 'node', '-e', "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 5s timeout: 3s retries: 12 start_period: 5s depends_on: api: condition: service_healthy stop_grace_period: 30s restart: unless-stopped volumes: data: