# Two disposable containers. All durable state is in the named volumes below.
#
# Laptop vs server: set BIND_ADDR (127.0.0.1 vs a VPN interface). Never 0.0.0.0.
#
#   BIND_ADDR=127.0.0.1 ./deploy/up.sh up --build
#
# synapse-core shares dsh's network namespace so MCP can bind 127.0.0.1 (the existing
# local-only guard) while dsh still reaches it. Rosters travel as files on `skills`.
#
# WHAT IS NOT HERE ANY MORE, and how to get it back:
#
#   vpn-sidecar — was an idle busybox until someone set VPN_IMAGE, which nobody did. A tunnel is
#     not this file's job: run Tailscale/WireGuard on the HOST and point BIND_ADDR at its interface
#     address. That is fewer moving parts and the same privacy posture, because the guarantee was
#     never "a VPN container exists" — it is "BIND_ADDR is not a wildcard" ([[doc-deployment-gate]]).
#
#   ollama — was behind `profiles: [embeddings]`, so it never started unless asked for. The
#     CAPABILITY is unchanged: SYNAPSE_OLLAMA_URL still points core at an embedding host, and it now
#     defaults to one on the Docker host. Semantic recall works; the container managing it does not
#     have to live here. The deterministic core never needed it either way.

name: synapse

services:
  dsh:
    image: ${DSH_IMAGE:-synapse-dsh-stub}
    build:
      context: ./dsh-stub
    container_name: synapse-dsh
    init: true
    restart: unless-stopped
    ports:
      - "${BIND_ADDR}:8080:8080"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - skills:/skills:ro
      - vaults:/synapse/vaults
      - dsh-home:/dsh-home
    environment:
      DSH_HOME: /dsh-home
      SYNAPSE_SKILLS_ROOT: /skills
      SYNAPSE_MCP_HTTP_URL: ${SYNAPSE_MCP_HTTP_URL:-http://127.0.0.1:3000/mcp}
      SYNAPSE_MCP_URL: ${SYNAPSE_MCP_URL:-http://127.0.0.1:3000/mcp/synapse-vault}
      SYNAPSE_MCP_TOKEN: ${SYNAPSE_MCP_TOKEN:-}
      # Extra authorities DSH's /api trust fence accepts, beyond loopback. Needed ONLY when the UI
      # is reached through a proxy on a real domain: requests then carry that domain in Host, the
      # fence answers 403, and the UI loads but cannot list providers.
      DSH_TRUSTED_HOSTS: ${DSH_TRUSTED_HOSTS:-}
      OPENCODE_GO_API_KEY: ${OPENCODE_GO_API_KEY:-}
    networks:
      - synapse

  synapse-core:
    build:
      # Repo root, reached from THIS file's directory. Compose resolves every relative path against
      # the project directory, which defaults to the compose file's parent — NOT your shell's cwd. A
      # root-relative `context: .` here silently becomes deploy/, and the build fails looking for
      # deploy/deploy/Dockerfile. deploy/compose.test.mjs pins this.
      context: ..
      dockerfile: deploy/Dockerfile
    container_name: synapse-core
    init: true
    restart: unless-stopped
    network_mode: service:dsh
    depends_on:
      - dsh
    # WHY container_name + replicas 1: `docker compose up --scale synapse-core=2` then collides
    # on the name. The process-level lock in lib/core-lock.mjs is the same rule for a host process.
    deploy:
      mode: replicated
      replicas: 1
    volumes:
      - vaults:/synapse/vaults
      - config:/synapse/config
      - skills:/synapse/skills
    environment:
      SYNAPSE_HOME: /synapse/config
      SYNAPSE_SKILLS_ROOT: /synapse/skills
      SYNAPSE_MCP_HOST: "127.0.0.1"
      SYNAPSE_MCP_PORT: "3000"
      SYNAPSE_MCP_PATH: /mcp
      SYNAPSE_MCP_SURFACE: orchestrator
      # An embedding host, not a container in this file. Defaults to one on the Docker host.
      #
      # No extra_hosts here: a container sharing another's network namespace cannot have its own
      # host-to-IP mappings (Docker refuses outright — "conflicting options: custom host-to-IP
      # mapping and the network mode"). /etc/hosts belongs to the namespace, so core resolves
      # host.docker.internal through the entry dsh declares.
      SYNAPSE_OLLAMA_URL: ${SYNAPSE_OLLAMA_URL:-http://host.docker.internal:11434}

volumes:
  vaults:
  config:
  skills:
  dsh-home:

networks:
  synapse:
    driver: bridge
