docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# 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