# Meerkat on Docker Swarm - three gateways serving ONE installation.
#
#   export MEERKAT_DATABASE_URL='postgres://meerkat:PASSWORD@postgres:5432/meerkat?sslmode=require'
#   export MEERKAT_VAULT_KEY="$(openssl rand -hex 32)"   # once, then keep it
#   export MEERKAT_ADMIN_PASSWORD='choose-one'
#   docker stack deploy -c stack.swarm.yml meerkat
#
# Swarm interpolates those at deploy time, from the shell that deploys: they
# never sit in this file, and this file goes into git.
#
# What makes three tasks ONE gateway is the shared database, and nothing else.
# The nodes never address each other: one writes a route, bumps a version and
# sends a NOTIFY, and the others hear it on the connection they already hold.
# So there is no volume here, no Redis, no broker, and no sticky session to
# ask of the routing mesh - sessions are rows, and a task that leaves takes
# nobody's session with it.
#
# For ONE gateway on one node, use docker-compose.yml (community) or
# docker-compose.ee.yml (Enterprise, with the developer tunnel): a single
# instance on its own volume needs no database at all.
services:
  meerkat:
    # The external database is an Enterprise capability: the community image
    # links no PostgreSQL driver, and says so in a sentence rather than
    # failing on one. Pin a release rather than following latest on what holds
    # your front door.
    # MEERKAT_IMAGE picks another edition or a pinned version without editing
    # this file: softwarity/meerkat:eval, or the image built for your licence.
    image: ${MEERKAT_IMAGE:-ghcr.io/softwarity/meerkat:latest}
    ports:
      # Published on the routing mesh: any node answers, whichever task runs.
      - target: 8080
        published: 8080
        mode: ingress
      # The control plane. Publishing it at all is a choice - keep it on an
      # internal network, or drop these three lines and reach the console
      # through an overlay network or an SSH tunnel.
      - target: 9090
        published: 9090
        mode: ingress
      # 7070, the internal plane, is not published: your services reach it
      # over the stack's overlay network, as http://meerkat:7070.
    environment:
      # The shared state. This line is what makes three tasks one gateway.
      MEERKAT_DATABASE_URL: ${MEERKAT_DATABASE_URL:?point it at a PostgreSQL every node can reach}
      # The vault's master key, THE SAME on every task. It seals what is IN
      # the database, so it is not kept there. Generate it once and keep it:
      # losing it loses every secret in the vault.
      MEERKAT_VAULT_KEY: ${MEERKAT_VAULT_KEY:?generate one with openssl rand -hex 32, then keep it}
      # Read once, on the FIRST start, to create the admin account: the task
      # that arrives first creates it, the others find it already there.
      MEERKAT_ADMIN_PASSWORD: ${MEERKAT_ADMIN_PASSWORD:?set a password for the first admin}
      # The runtime, through the read-only proxy below, on every node: the
      # route editor offers the services it finds, and the routes screen shows
      # each target's ready replicas and the image they run, as Swarm's events
      # say - no timer.
      DOCKER_HOST: tcp://docker-proxy:2375
    networks:
      - meerkat
      - runtime
    deploy:
      replicas: 3
      placement:
        # One task per node, or three tasks on one node answer that node's
        # failure exactly as one task would.
        max_replicas_per_node: 1
      update_config:
        # The new task is up before the old one goes: the state is shared, so
        # both can serve at once.
        order: start-first
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: any
    healthcheck:
      # READINESS, not liveness: it pings the store and checks the routing
      # table is compiled. A task that accepted connections before compiling
      # its table answers 404 to everything, and a rolling update would read
      # that as a healthy instance.
      # The image has no shell and no wget: the gateway asks itself.
      test: ["CMD", "/meerkat", "health", "http://127.0.0.1:8080/readyz"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 15s

  # The Docker socket of EVERY node, behind a filter that refuses every write.
  # Every node, because a container's events are emitted by the node it runs
  # on only: the gateway listens to each proxy task (tasks.docker-proxy) and
  # sees a replica die wherever it ran. The managers' proxies are also the
  # ones that can list the Swarm services; the gateway asks each in turn.
  # Mounting the socket in the gateway instead would tie it to the managers
  # and hand whoever controls it the whole host.
  docker-proxy:
    image: tecnativa/docker-socket-proxy:v0.4.1
    environment:
      # What it lets through: reads only (POST stays 0, the default), and only
      # what the gateway reads - the services and their tasks, the containers,
      # the networks, the images for their declared ports, the nodes, and the
      # event stream that says when any of them changes.
      SERVICES: 1
      TASKS: 1
      CONTAINERS: 1
      NETWORKS: 1
      IMAGES: 1
      NODES: 1
      EVENTS: 1
      INFO: 1
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - runtime
    deploy:
      mode: global
      restart_policy:
        condition: any

# The developer tunnel is not here on purpose. It plugs one machine into the
# cluster by taking a service name, which means acting on the orchestrator:
# that is a grant (the Docker socket) and it belongs on the node that holds
# it, not spread over three. Open it on a single-gateway stack -
# docker-compose.ee.yml - or on a one-node Swarm.

networks:
  meerkat:
    driver: overlay
    attachable: true
  # The proxies' network, which only the gateway joins: none of the
  # applications it routes to can reach the Docker API.
  runtime:
    driver: overlay
    internal: true
