Ctrl K
ComunidadGuías…

docker-compose_swarm.yml

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

Archivo original ↗Preguntar a KiBot
services:
  db-prod:
    container_name: alexclaw-db-prod
    image: pgvector/pgvector:pg17
    restart: unless-stopped
    environment:
      # The owner. Used only when the volume is first initialised; the
      # application role is created by the operator — see the upgrade guide.
      POSTGRES_USER: ${DATABASE_OWNER_USERNAME:-alexclaw}
      POSTGRES_PASSWORD: ${DATABASE_OWNER_PASSWORD}
      POSTGRES_DB: alex_claw_prod
      # For db-init/01-app-role.sh, which creates the application role on a
      # fresh volume. An existing volume ignores both.
      DATABASE_APP_USERNAME: "${DATABASE_USERNAME:?not set - the application role}"
      DATABASE_APP_PASSWORD: "${DATABASE_PASSWORD:?not set - the application role's password}"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./db-init/01-app-role.sh:/docker-entrypoint-initdb.d/01-app-role.sh:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DATABASE_OWNER_USERNAME:-alexclaw}"]
      interval: 5s
      timeout: 3s
      retries: 5

  # Migrations run here, as the database owner, and nowhere else. This is the
  # only container given the owner's credentials: it migrates, grants the
  # application role its privileges (AlexClaw.Database.Roles), and exits. The
  # application starts only once it has exited cleanly, and connects as the
  # application role — which it checks at boot.
  migrate:
    container_name: alexclaw-migrate
    image: alexclaw-app:latest
    build:
      context: .
      args:
        BUILD_NUMBER: ${BUILD_NUMBER:-0}
    command: ["bin/alex_claw", "eval", "AlexClaw.Release.migrate()"]
    restart: "no"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    depends_on:
      db-prod:
        condition: service_healthy
    environment:
      DATABASE_USERNAME: ${DATABASE_OWNER_USERNAME:-alexclaw}
      DATABASE_PASSWORD: "${DATABASE_OWNER_PASSWORD:?not set - the database owner's password}"
      DATABASE_HOSTNAME: db-prod
      DATABASE_APP_USERNAME: "${DATABASE_USERNAME:?not set - the application role}"
      # No SECRET_KEY_BASE, deliberately: the owner's credentials and the key
      # that decrypts the stored secrets never share a container. The runtime
      # configuration accepts its absence for an eval only.
      CLUSTER_COOKIE: "${CLUSTER_COOKIE:?not set - generate one with: openssl rand -base64 32}"
      RELEASE_TMP: /tmp
      ERL_CRASH_DUMP: /tmp/erl_crash.dump

  node1:
    # The BEAM turns SIGTERM into an orderly System.stop/0, but Docker's default
    # grace period is 10 seconds and the supervision tree needs longer: every
    # stop was reaching SIGKILL, so Reasoning.Loop's terminate/2 never ran and
    # in-flight workflow runs stayed recorded as executing.
    stop_grace_period: 30s
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    image: alexclaw-app:latest
    build:
      context: .
      args:
        BUILD_NUMBER: ${BUILD_NUMBER:-0}
    hostname: node1.local
    restart: unless-stopped
    # A ceiling, so a node can never take the host down with it. Local models
    # run on the host, outside this limit.
    mem_limit: 2g
    cpus: 2.0
    depends_on:
      migrate:
        condition: service_completed_successfully
    ports:
      - "${ADMIN_BIND:-127.0.0.1}:5001:5001"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      # The application role, never the owner: the boot refuses the owner.
      DATABASE_USERNAME: "${DATABASE_USERNAME:?not set - the application role}"
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
      DATABASE_HOSTNAME: db-prod
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      # The published port above: the origin the browser sends.
      ADMIN_PORT: "5001"
      PHX_HOST: ${PHX_HOST:-}
      CHECK_ORIGIN: ${CHECK_ORIGIN:-}
      TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
      OLLAMA_ENABLED: ${OLLAMA_ENABLED:-false}
      OLLAMA_HOST: ${OLLAMA_HOST:-http://host.docker.internal:11434}
      OLLAMA_MODEL: ${OLLAMA_MODEL:-}
      LMSTUDIO_ENABLED: ${LMSTUDIO_ENABLED:-false}
      LMSTUDIO_HOST: ${LMSTUDIO_HOST:-http://host.docker.internal:1234}
      LMSTUDIO_MODEL: ${LMSTUDIO_MODEL:-}
      NODE_NAME: alexclaw@node1.local
      CLUSTER_COOKIE: "${CLUSTER_COOKIE:?not set - generate one with: openssl rand -base64 32}"
      SKILLS_DIR: /app/skills
      RELEASE_TMP: /tmp
      ERL_CRASH_DUMP: /tmp/erl_crash.dump
    volumes:
      - skills_data:/app/skills

  node2:
    # The BEAM turns SIGTERM into an orderly System.stop/0, but Docker's default
    # grace period is 10 seconds and the supervision tree needs longer: every
    # stop was reaching SIGKILL, so Reasoning.Loop's terminate/2 never ran and
    # in-flight workflow runs stayed recorded as executing.
    stop_grace_period: 30s
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    image: alexclaw-app:latest
    build:
      context: .
      args:
        BUILD_NUMBER: ${BUILD_NUMBER:-0}
    hostname: node2.local
    restart: unless-stopped
    # A ceiling, so a node can never take the host down with it. Local models
    # run on the host, outside this limit.
    mem_limit: 2g
    cpus: 2.0
    depends_on:
      migrate:
        condition: service_completed_successfully
    ports:
      - "${ADMIN_BIND:-127.0.0.1}:5002:5001"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      # The application role, never the owner: the boot refuses the owner.
      DATABASE_USERNAME: "${DATABASE_USERNAME:?not set - the application role}"
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
      DATABASE_HOSTNAME: db-prod
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      # The published port above: the origin the browser sends.
      ADMIN_PORT: "5002"
      PHX_HOST: ${PHX_HOST:-}
      CHECK_ORIGIN: ${CHECK_ORIGIN:-}
      TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
      OLLAMA_ENABLED: ${OLLAMA_ENABLED:-false}
      OLLAMA_HOST: ${OLLAMA_HOST:-http://host.docker.internal:11434}
      OLLAMA_MODEL: ${OLLAMA_MODEL:-}
      LMSTUDIO_ENABLED: ${LMSTUDIO_ENABLED:-false}
      LMSTUDIO_HOST: ${LMSTUDIO_HOST:-http://host.docker.internal:1234}
      LMSTUDIO_MODEL: ${LMSTUDIO_MODEL:-}
      NODE_NAME: alexclaw@node2.local
      CLUSTER_COOKIE: "${CLUSTER_COOKIE:?not set - generate one with: openssl rand -base64 32}"
      SKILLS_DIR: /app/skills
      RELEASE_TMP: /tmp
      ERL_CRASH_DUMP: /tmp/erl_crash.dump
    volumes:
      - skills_data:/app/skills

volumes:
  pgdata:
  skills_data: