Ctrl K
ComunidadGuías…

docker-compose.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# MikeOSS — full local stack, one `docker compose up`. No external accounts.
# Supabase is embedded (db + auth + rest + gateway); storage is RustFS.
# The only thing you must supply is one model API key in backend/.env.
#
# JWT secret + anon/service_role keys below are the well-known Supabase local
# demo values — fine for localhost, NOT for anything exposed. The keys are JWTs
# signed with the secret; don't change one without regenerating the others.

services:
  # --- Supabase: Postgres (ships base roles, the `auth` schema, auth.uid, etc.)
  db:
    image: supabase/postgres:17.6.1.136
    ports:
      # ponytail: infra ports bind loopback only — reachable from the host for
      # debugging, never from the internet. Compose networking is unaffected, so
      # the other services still reach these by service name.
      - "127.0.0.1:${DB_PORT:-54322}:5432" # psql from host if needed
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
      JWT_SECRET: super-secret-jwt-token-with-at-least-32-characters-long
      JWT_EXP: 3600
    volumes:
      - db_data:/var/lib/postgresql/data
      - ./docker/db-init/roles.sql:/docker-entrypoint-initdb.d/zz-roles.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 20
    restart: unless-stopped

  # --- Supabase: Auth (GoTrue). Auto-runs its migrations to build auth.users.
  auth:
    image: supabase/gotrue:v2.189.0
    environment:
      GOTRUE_API_HOST: 0.0.0.0
      GOTRUE_API_PORT: "9999"
      # Must be the browser-reachable gateway URL — confirmation links are built
      # from it, so a localhost value makes signup uncompletable off-machine.
      API_EXTERNAL_URL: ${SUPABASE_PUBLIC_URL:-http://localhost:54321}
      GOTRUE_DB_DRIVER: postgres
      GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:postgres@db:5432/postgres
      GOTRUE_SITE_URL: ${FRONTEND_URL:-http://localhost:3000}
      GOTRUE_URI_ALLOW_LIST: "*"
      GOTRUE_JWT_SECRET: super-secret-jwt-token-with-at-least-32-characters-long
      GOTRUE_JWT_EXP: "3600"
      GOTRUE_JWT_AUD: authenticated
      GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
      GOTRUE_JWT_ADMIN_ROLES: service_role
      GOTRUE_PASSWORD_MIN_LENGTH: "10"
      # Open registration by default (localhost). On a public host, create your
      # account then set GOTRUE_DISABLE_SIGNUP=true to pull the ladder up.
      GOTRUE_DISABLE_SIGNUP: ${GOTRUE_DISABLE_SIGNUP:-false}
      GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
      GOTRUE_EXTERNAL_PHONE_ENABLED: "false"
      GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOTRUE_EXTERNAL_GOOGLE_ENABLED:-true}
      GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID:-}
      GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOTRUE_EXTERNAL_GOOGLE_SECRET:-}
      GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${SUPABASE_PUBLIC_URL:-http://localhost:54321}/auth/v1/callback
      # Enterprise SAML is opt-in. Set these in the Compose environment/root
      # .env (not backend/.env); see docs/deployment.md for provider setup.
      # GOTRUE_SAML_ENABLED: "true"
      # GOTRUE_SAML_PRIVATE_KEY: ${GOTRUE_SAML_PRIVATE_KEY:-}
      # GOTRUE_SAML_EXTERNAL_URL: ${SUPABASE_PUBLIC_URL:-http://localhost:54321}/auth/v1
      # Off by default: shares and organization invitations are addressed by
      # email, so an unconfirmed signup must not be able to claim an address
      # it does not own. Confirmation mail is caught by Mailpit locally. Set
      # this to true only for a throwaway local stack.
      GOTRUE_MAILER_AUTOCONFIRM: ${GOTRUE_MAILER_AUTOCONFIRM:-false}
      GOTRUE_SMTP_HOST: mailpit
      GOTRUE_SMTP_PORT: "1025"
      # No SMTP_USER/PASS: Mailpit needs no auth, and GoTrue refuses to send
      # credentials over a plaintext (non-TLS) connection ("unencrypted connection").
      GOTRUE_SMTP_ADMIN_EMAIL: admin@mike.local
      GOTRUE_SMTP_SENDER_NAME: Mike
      # Confirmation/recovery links resolve to the gateway, which strips /auth/v1.
      GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
      GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
      GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify
      GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
    depends_on:
      db:
        condition: service_healthy
      mailpit:
        condition: service_started
    restart: unless-stopped

  # --- Supabase: data API (PostgREST). Backend queries tables through this.
  rest:
    image: postgrest/postgrest:v14.12
    depends_on:
      db:
        condition: service_healthy
    environment:
      PGRST_DB_URI: postgres://authenticator:postgres@db:5432/postgres
      PGRST_DB_SCHEMAS: public
      PGRST_DB_ANON_ROLE: anon
      PGRST_JWT_SECRET: super-secret-jwt-token-with-at-least-32-characters-long
    restart: unless-stopped

  # --- Supabase: gateway. The backend reaches Auth/PostgREST through here.
  gateway:
    image: nginx:alpine
    ports:
      # Public only so confirmation links generated by the local Auth service work.
      - "${GATEWAY_PORT:-54321}:8000"
    volumes:
      - ./supabase/gateway.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - auth
      - rest
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "wget -q --spider http://127.0.0.1:8000/rest/v1/ || exit 1",
        ]
      interval: 5s
      timeout: 5s
      retries: 20
    restart: unless-stopped

  # --- One-shot: load schema.sql on a new volume, then apply any pending
  # migrations with backend/scripts/migrate.sh, which records each one in
  # public.schema_migrations so the next start skips it; grant service_role.
  # Runs from the backend image (which carries schema.sql, migrations/ and the
  # scripts, plus psql) rather than bind-mounting them from the checkout, so a
  # pulled image can never be paired with another version's migrations.
  db-init:
    # Every backend consumer (db-init, workflow-sync, backend, worker) runs the
    # same image, so code, schema and migrations always come from one release.
    # The default tag is the version in /VERSION; `docker compose up --build`
    # still builds it from source (backend/src/__tests__/releaseVersion.test.ts
    # keeps the two in step).
    build: &backend-build
      context: .
      dockerfile: backend/Dockerfile
      args:
        # Release identity for Sentry (docs/observability.md): build args, not
        # runtime variables, because the Dockerfile bakes them into the image.
        MIKE_VERSION: ${MIKE_VERSION:-}
        GIT_SHA: ${GIT_SHA:-}
    image: &backend-image ${MIKE_IMAGE_REPO:-ghcr.io/open-legal-products}/mike-backend:${MIKE_IMAGE_TAG:-2.1.0}
    depends_on:
      db:
        condition: service_healthy
      auth:
        condition: service_started
    environment:
      DB: postgres://postgres:postgres@db:5432/postgres
      DATABASE_URL: postgres://postgres:postgres@db:5432/postgres
      MIGRATIONS_DIR: /app/migrations
      # adopt-ledger.sh (pre-ledger volumes) runs migrate.sh from here too.
      MIGRATE_SH: /app/scripts/migrate.sh
    entrypoint: ["bash", "-c"]
    command:
      - |
        until psql "$$DB" -tAc "select to_regclass('auth.users')" | grep -q auth; do
          echo "waiting for auth schema..."; sleep 2;
        done
        if ! psql "$$DB" -tAc "select to_regclass('public.user_profiles')" | grep -q user_profiles; then
          # New volume: schema.sql is the complete schema, and it records every
          # migration it already contains in the ledger.
          psql "$$DB" -v ON_ERROR_STOP=1 -f /app/schema.sql || exit 1;
        elif ! psql "$$DB" -tAc "select to_regclass('public.schema_migrations')" | grep -q schema_migrations; then
          # Volume from before the ledger: replay one last time the way
          # db-init used to on every start, then record it.
          bash /app/docker/db-init/adopt-ledger.sh || exit 1;
        fi
        bash /app/scripts/migrate.sh up || exit 1;
        psql "$$DB" -c "grant usage on schema public to service_role;
          grant all privileges on all tables in schema public to service_role;
          grant all privileges on all sequences in schema public to service_role;
          notify pgrst, 'reload schema';"
    restart: "no"

  # --- Local SMTP catcher. Auth emails land in its web inbox (no real sending).
  mailpit:
    image: axllent/mailpit:latest
    ports:
      # Loopback only: the inbox exposes auth + password-reset links.
      - "127.0.0.1:${MAILPIT_PORT:-8025}:8025" # web inbox
      - "127.0.0.1:${MAILPIT_SMTP_PORT:-1025}:1025" # SMTP
    environment:
      MP_SMTP_AUTH_ACCEPT_ANY: "1"
      MP_SMTP_AUTH_ALLOW_INSECURE: "1"
    restart: unless-stopped

  # --- S3-compatible object storage (RustFS, MinIO alternative).
  storage:
    image: rustfs/rustfs:latest
    ports:
      # Loopback only: creds below are the default admin pair.
      - "127.0.0.1:${STORAGE_PORT:-9000}:9000"
      - "127.0.0.1:${STORAGE_CONSOLE_PORT:-9001}:9001"
    environment:
      RUSTFS_VOLUMES: /data
      RUSTFS_ADDRESS: 0.0.0.0:9000
      RUSTFS_CONSOLE_ADDRESS: 0.0.0.0:9001
      RUSTFS_CONSOLE_ENABLE: "true"
      RUSTFS_ACCESS_KEY: rustfsadmin
      RUSTFS_SECRET_KEY: rustfsadmin
    volumes:
      - storage_data:/data
    restart: unless-stopped

  # Writes the bucket CORS policy createbucket installs: browsers PUT uploads
  # straight to storage, so the bucket must allow every origin the backend
  # trusts (FRONTEND_URL, WORD_ADDIN_URL, ALLOWED_ORIGINS). It runs the
  # backend image's own origin parser (docker/storage-cors.mjs) with the
  # backend's env_file and the same overrides as the backend service below,
  # so the two allowlists cannot disagree. Keep these three blocks in step
  # with the backend service (backend/src/__tests__/storageCors.test.ts).
  storage-cors:
    build: *backend-build
    image: *backend-image
    env_file:
      - path: ./backend/.env
        required: false
      - path: .env
        required: false
    environment:
      - NODE_ENV=${NODE_ENV:-development}
      - FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}
      - MIKE_ORIGINS_MODULE=/app/dist/lib/origins.js
    volumes:
      - ./docker/storage-cors.mjs:/storage-cors.mjs:ro
      - storage_cors:/cors
    entrypoint: /bin/sh
    command: ["-c", "node /storage-cors.mjs > /cors/storage-cors.json"]
    # It reads the backend's env_file (provider keys and signing secrets
    # included) only to print a JSON policy, so it gets no network at all.
    network_mode: none
    restart: "no"

  createbucket:
    image: amazon/aws-cli
    depends_on:
      storage:
        condition: service_started
      storage-cors:
        condition: service_completed_successfully
    environment:
      AWS_ACCESS_KEY_ID: rustfsadmin
      AWS_SECRET_ACCESS_KEY: rustfsadmin
      AWS_DEFAULT_REGION: us-east-1
    volumes:
      - storage_cors:/cors:ro
    entrypoint: /bin/sh
    command: >
      -c "until aws --endpoint-url http://storage:9000 s3 ls; do echo 'waiting for storage'; sleep 2; done;
          aws --endpoint-url http://storage:9000 s3 mb s3://mike || true;
          aws --endpoint-url http://storage:9000 s3api put-bucket-cors --bucket mike --cors-configuration file:///cors/storage-cors.json"
    restart: "no"

  # --- Redis: backs the BullMQ job queues (conversion, extraction, fast
  # delivery for background jobs). NEW INSTALLS get it out of the box, which
  # is why the backend below ships with the ASYNC_* flags on. Existing
  # deployments that upgrade in place are untouched: the flags only default
  # on here in compose, never in code, and without Redis configured the
  # backend falls back to its Postgres-backed queue automatically.
  # AOF persistence is on so queued jobs survive a Redis restart.
  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes"]
    ports:
      # Loopback only — debugging convenience, never internet-reachable.
      - "127.0.0.1:${REDIS_PORT:-6379}:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped

  # --- One-shot: download, validate, and transactionally import mike-workflows.
  workflow-sync:
    build: *backend-build
    image: *backend-image
    env_file:
      - path: ./backend/.env
        required: false
      - path: .env
        required: false
    environment:
      - SUPABASE_URL=http://gateway:8000
      - SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY:-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU}
      - R2_ENDPOINT_URL=http://storage:9000
      - R2_ACCESS_KEY_ID=rustfsadmin
      - R2_SECRET_ACCESS_KEY=rustfsadmin
      - R2_BUCKET_NAME=mike
    command: ["npm", "run", "sync:workflows"]
    depends_on:
      db-init:
        condition: service_completed_successfully
      createbucket:
        condition: service_completed_successfully
      gateway:
        condition: service_healthy
    restart: "no"
  backend:
    build: *backend-build
    image: *backend-image
    ports:
      - "${BACKEND_PORT:-3001}:3001"
    # ponytail: both optional — local dev keeps secrets in backend/.env, hosted
    # deploys (Dokploy et al) write them to the compose-root .env. Whichever
    # exists wins; root .env is last so it takes precedence. Needs Compose 2.24+.
    #
    # Secrets must NOT be listed in `environment:` below — that section overrides
    # env_file, so a bare name unset on the host blanks out the file's value.
    env_file:
      - path: ./backend/.env
        required: false
      - path: .env
        required: false
    environment:
      # Topology + local Supabase wiring (override .env placeholders).
      - PORT=3001
      - NODE_ENV=${NODE_ENV:-development}
      - FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}
      - API_PUBLIC_URL=${API_PUBLIC_URL:-http://localhost:3000/api}
      - SUPABASE_URL=http://gateway:8000
      - SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY:-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0}
      - SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY:-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU}
      - R2_ENDPOINT_URL=http://storage:9000
      # The storage URL BROWSERS reach for direct uploads/downloads. The
      # default only works when the app is opened on this machine; for any
      # other host set it to a public (https under an https front end) URL
      # that reaches the storage service. See docs/deployment.md.
      - R2_PUBLIC_ENDPOINT_URL=${R2_PUBLIC_ENDPOINT_URL:-http://localhost:${STORAGE_PORT:-9000}}
      - R2_ACCESS_KEY_ID=rustfsadmin
      - R2_SECRET_ACCESS_KEY=rustfsadmin
      - R2_BUCKET_NAME=mike
      # Local Ollama running on the host. Override OLLAMA_MODEL to change the tag.
      - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434/v1}
      # Durable job queues: fresh compose installs run BullMQ-on-Redis for
      # everything (conversion + extraction async, instant delivery for
      # background jobs). Deployments without Redis fall back to the
      # Postgres queue; these flags are compose-level so in-place upgrades
      # of non-compose installs see no behavior change.
      - REDIS_URL=${REDIS_URL:-redis://redis:6379}
      - ASYNC_DOCUMENT_CONVERSION=${ASYNC_DOCUMENT_CONVERSION:-true}
      - ASYNC_TABULAR_EXTRACTION=${ASYNC_TABULAR_EXTRACTION:-true}
      # Background workers run in a worker_thread inside this container by
      # default (off the HTTP event loop). To move them to their own
      # container/machine, set WORKERS_MODE=none here and enable the
      # `worker` service below.
      - WORKERS_MODE=${WORKERS_MODE:-thread}
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      workflow-sync:
        condition: service_completed_successfully
      auth:
        condition: service_started
      rest:
        condition: service_started
      redis:
        condition: service_healthy
    restart: unless-stopped

  # --- Optional: dedicated worker process ("async computer" mode).
  # Same image as the backend, different command: it runs ONLY the queue
  # workers and maintenance sweeps against the same Postgres/Redis. To use:
  # uncomment this service AND set WORKERS_MODE=none on the backend above so
  # background work runs exactly once. Scale-out is safe by construction —
  # BullMQ partitions per connection and the DB queue claims with
  # FOR UPDATE SKIP LOCKED, so replicas divide jobs, never duplicate them.
  # worker:
  #   build: *backend-build
  #   image: *backend-image
  #   command: ["node", "dist/worker.js"]
  #   env_file:
  #     - path: ./backend/.env
  #       required: false
  #     - path: .env
  #       required: false
  #   environment:
  #     - SUPABASE_URL=http://gateway:8000
  #     - SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY:-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU}
  #     - R2_ENDPOINT_URL=http://storage:9000
  #     - R2_ACCESS_KEY_ID=rustfsadmin
  #     - R2_SECRET_ACCESS_KEY=rustfsadmin
  #     - R2_BUCKET_NAME=mike
  #     - REDIS_URL=${REDIS_URL:-redis://redis:6379}
  #     - ASYNC_DOCUMENT_CONVERSION=${ASYNC_DOCUMENT_CONVERSION:-true}
  #     - ASYNC_TABULAR_EXTRACTION=${ASYNC_TABULAR_EXTRACTION:-true}
  #   depends_on:
  #     db-init:
  #       condition: service_completed_successfully
  #     redis:
  #       condition: service_healthy
  #   restart: unless-stopped

  frontend:
    build:
      # Repo-root context: the frontend's shared types import type-only
      # definitions from backend/src (see frontend/Dockerfile).
      context: .
      dockerfile: frontend/Dockerfile
      args:
        # Release identity (docs/observability.md), as for the backend image.
        MIKE_VERSION: ${MIKE_VERSION:-}
        # The browser-side Sentry DSN is inlined by `next build`, so it is a
        # build argument, not a runtime variable (docs/observability.md).
        NEXT_PUBLIC_SENTRY_DSN: ${FRONTEND_SENTRY_DSN:-}
        NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-}
        NEXT_PUBLIC_SENTRY_RELEASE: ${SENTRY_RELEASE:-}
        # Opt-out and install kind reach the browser bundle at build time.
        NEXT_PUBLIC_SENTRY_DISABLED: ${SENTRY_DISABLED:-}
        NEXT_PUBLIC_SENTRY_INSTALL: ${SENTRY_INSTALL:-}
        GIT_SHA: ${GIT_SHA:-}
        # Sentry source-map upload (docs/observability.md); the token is the
        # build secret below.
        SENTRY_ORG: ${SENTRY_ORG:-}
        SENTRY_PROJECT: ${SENTRY_PROJECT:-}
        SENTRY_UPLOAD_ENABLED: ${SENTRY_UPLOAD_ENABLED:-0}
      secrets:
        - SENTRY_AUTH_TOKEN
    # Same default tag as the backend image; pull both or build both.
    image: ${MIKE_IMAGE_REPO:-ghcr.io/open-legal-products}/mike-frontend:${MIKE_IMAGE_TAG:-2.1.0}
    environment:
      - API_BASE_URL=${API_BASE_URL:-http://backend:3001}
      # Next server side (the /api gateway); same project as the browser.
      - SENTRY_DISABLED=${SENTRY_DISABLED:-}
      - SENTRY_INSTALL=${SENTRY_INSTALL:-}
      - SENTRY_DSN=${FRONTEND_SENTRY_DSN:-}
      - SENTRY_MAX_REPEAT_EVENTS_PER_DAY=${SENTRY_MAX_REPEAT_EVENTS_PER_DAY:-}
      - SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-}
      - SENTRY_RELEASE=${SENTRY_RELEASE:-}
    ports:
      # Dokploy's own dashboard occupies 3000 — set FRONTEND_PORT there.
      - "${FRONTEND_PORT:-3000}:3000"
    depends_on:
      - backend
    restart: unless-stopped

volumes:
  db_data:
  storage_data:
  storage_cors:
  redis_data:

secrets:
  SENTRY_AUTH_TOKEN:
    # Must be exported/injected into the Compose process, not only in .env.
    environment: SENTRY_AUTH_TOKEN