Hookway

Docker compose — production-local/docker-compose

# Persistent local deployment. Bootstrap and operate with make prod-local-*.
# No service publishes credentials or private control/metrics/database listeners.
x-runtime: &runtime
  restart: unless-stopped
  stop_grace_period: 70s
  mem_limit: ${HOOKWAY_MEMORY_LIMIT:-256m}
  cpus: ${HOOKWAY_CPU_LIMIT:-1.0}
  pids_limit: 128
  security_opt: [no-new-privileges:true]
x-replay-environment: &replay-environment
  REPLAY_DATABASE_URL: ${REPLAY_RUNTIME_DATABASE_URL:-}
  REPLAY_CAPTURE_TOKEN: ${REPLAY_CAPTURE_TOKEN:?Run make prod-local-bootstrap first}
  REPLAY_CONTROL_TOKEN: ${REPLAY_CONTROL_TOKEN:?Run make prod-local-bootstrap first}
  REPLAY_OPERATOR_TOKEN: ${REPLAY_OPERATOR_TOKEN:?Run make prod-local-bootstrap first}
  REPLAY_METRICS_TOKEN: ${REPLAY_METRICS_TOKEN:?Run make prod-local-bootstrap first}
  REPLAY_CONSOLE_ORIGIN: ${REPLAY_CONSOLE_ORIGIN:-http://localhost:18082}

services:
  rabbitmq:
    image: ${RABBITMQ_IMAGE:-rabbitmq:3-management}
    user: rabbitmq
    hostname: rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: hookway
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?Run make prod-local-bootstrap first}
    volumes: [rabbitmq-data:/var/lib/rabbitmq]
    ports: ["127.0.0.1:${RABBITMQ_HTTP_PORT:-15673}:15672"]
    restart: unless-stopped
    stop_grace_period: 60s
    mem_limit: ${RABBITMQ_MEMORY_LIMIT:-512m}
    cpus: ${RABBITMQ_CPU_LIMIT:-1.0}
    pids_limit: 512
    healthcheck:
      test: [CMD, rabbitmq-diagnostics, -q, ping]
      interval: 5s
      timeout: 10s
      retries: 30

  hookway:
    <<: *runtime
    image: ${HOOKWAY_CORE_IMAGE:-hookway-production-core:local}
    build: {context: ../.., dockerfile: Dockerfile, target: core}
    read_only: true
    environment:
      HOOKWAY_CONFIG: /etc/hookway/core.yaml
      RABBITMQ_URL: ${RABBITMQ_URL:?Run make prod-local-bootstrap first}
      GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:?Run make prod-local-bootstrap first}
      HOOKWAY_API_KEY: ${HOOKWAY_API_KEY:?Run make prod-local-bootstrap first}
      REPLAY_CAPTURE_TOKEN: ${REPLAY_CAPTURE_TOKEN:?Run make prod-local-bootstrap first}
      REPLAY_CONTROL_TOKEN: ${REPLAY_CONTROL_TOKEN:?Run make prod-local-bootstrap first}
      PROTECTION_REDIS_URL: ${PROTECTION_REDIS_URL:?Run make prod-local-bootstrap first}
    volumes: ["${HOOKWAY_LOCAL_DIRECTORY:?Use make prod-local-bootstrap}/core.yaml:/etc/hookway/core.yaml:ro"]
    ports: ["127.0.0.1:${HOOKWAY_HTTP_PORT:-18080}:8080"]
    depends_on:
      rabbitmq: {condition: service_healthy}
    healthcheck:
      test: [CMD, /usr/local/bin/hookway, healthcheck, --url, "http://127.0.0.1:8080/readyz"]
      interval: 5s
      timeout: 4s
      start_period: 15s
      retries: 30

  notify-sink:
    profiles: [replay]
    image: hashicorp/http-echo:1.0
    user: "65532:65532"
    command: ["-listen=:8080", "-text=ok"]
    restart: unless-stopped
    mem_limit: 32m
    cpus: 0.25
    pids_limit: 32
    read_only: true

  postgres:
    profiles: [replay]
    image: ${POSTGRES_IMAGE:-postgres:17-alpine}
    user: postgres
    environment:
      POSTGRES_USER: replay
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Run make prod-local-bootstrap first}
      POSTGRES_DB: replay
    volumes: [postgres-data:/var/lib/postgresql/data]
    restart: unless-stopped
    stop_grace_period: 60s
    mem_limit: ${POSTGRES_MEMORY_LIMIT:-256m}
    cpus: ${POSTGRES_CPU_LIMIT:-0.5}
    pids_limit: 128
    healthcheck:
      test: [CMD, pg_isready, -U, replay, -d, replay]
      interval: 2s
      timeout: 3s
      retries: 30

  db-provision:
    profiles: [replay]
    image: ${POSTGRES_IMAGE:-postgres:17-alpine}
    user: postgres
    entrypoint: [psql]
    command: [--host, postgres, --username, replay, --dbname, replay, --no-psqlrc, --file, /etc/hookway/provision.sql]
    environment:
      PGPASSWORD: ${POSTGRES_PASSWORD}
      REPLAY_MIGRATION_PASSWORD: ${REPLAY_MIGRATION_PASSWORD:-}
      REPLAY_RUNTIME_PASSWORD: ${REPLAY_RUNTIME_PASSWORD:-}
    volumes: ["./database/provision.sql:/etc/hookway/provision.sql:ro"]
    restart: "no"
    read_only: true
    mem_limit: 64m
    cpus: 0.25
    pids_limit: 32
    security_opt: [no-new-privileges:true]
    depends_on:
      postgres: {condition: service_healthy}

  migrate:
    profiles: [replay]
    <<: *runtime
    image: ${HOOKWAY_REPLAY_IMAGE:-hookway-production-replay:local}
    build: {context: ../.., dockerfile: Dockerfile, target: replay}
    command: [migrate, --config, /etc/hookway/migrate.yaml]
    restart: "no"
    read_only: true
    environment:
      REPLAY_MIGRATION_DATABASE_URL: ${REPLAY_MIGRATION_DATABASE_URL:-}
      REPLAY_MIGRATION_PASSWORD: ${REPLAY_MIGRATION_PASSWORD:-}
    volumes: ["${HOOKWAY_LOCAL_DIRECTORY}/migrate.yaml:/etc/hookway/migrate.yaml:ro"]
    depends_on:
      db-provision: {condition: service_completed_successfully}

  db-grants:
    profiles: [replay]
    image: ${POSTGRES_IMAGE:-postgres:17-alpine}
    user: postgres
    entrypoint: [psql]
    command: [--host, postgres, --username, replay_owner, --dbname, replay, --no-psqlrc, --file, /etc/hookway/runtime-grants.sql]
    environment:
      PGPASSWORD: ${REPLAY_MIGRATION_PASSWORD:-}
    volumes: ["./database/runtime-grants.sql:/etc/hookway/runtime-grants.sql:ro"]
    restart: "no"
    read_only: true
    mem_limit: 64m
    cpus: 0.25
    pids_limit: 32
    security_opt: [no-new-privileges:true]
    depends_on:
      migrate: {condition: service_completed_successfully}

  replay:
    profiles: [replay]
    <<: *runtime
    image: ${HOOKWAY_REPLAY_IMAGE:-hookway-production-replay:local}
    build: {context: ../.., dockerfile: Dockerfile, target: replay}
    command: [serve, --config, /etc/hookway/replay.yaml]
    read_only: true
    environment: *replay-environment
    volumes: ["${HOOKWAY_LOCAL_DIRECTORY}/replay.yaml:/etc/hookway/replay.yaml:ro"]
    ports: ["127.0.0.1:${REPLAY_HTTP_PORT:-18081}:8080"]
    depends_on:
      db-grants: {condition: service_completed_successfully}
    healthcheck:
      test: [CMD, /usr/local/bin/hookway-replay, healthcheck, --url, "http://127.0.0.1:8080/readyz"]
      interval: 5s
      timeout: 4s
      start_period: 15s
      retries: 30

  console:
    profiles: [replay]
    <<: *runtime
    image: ${HOOKWAY_CONSOLE_IMAGE:-hookway-production-console:local}
    build: {context: ../.., dockerfile: web/Dockerfile}
    environment:
      REPLAY_API_URL: ${REPLAY_API_URL:-http://localhost:18081}
    ports: ["127.0.0.1:${CONSOLE_HTTP_PORT:-18082}:8080"]
    depends_on:
      replay: {condition: service_healthy}

  redis:
    profiles: [protection]
    image: ${REDIS_IMAGE:-redis:7-alpine}
    user: redis
    command: [redis-server, --appendonly, "yes", --requirepass, "${REDIS_PASSWORD:?Run make prod-local-bootstrap first}"]
    environment:
      REDISCLI_AUTH: ${REDIS_PASSWORD}
    volumes: [redis-data:/data]
    restart: unless-stopped
    stop_grace_period: 60s
    mem_limit: ${REDIS_MEMORY_LIMIT:-128m}
    cpus: ${REDIS_CPU_LIMIT:-0.5}
    pids_limit: 64
    healthcheck:
      test: [CMD, redis-cli, ping]
      interval: 2s
      timeout: 3s
      retries: 30

volumes:
  rabbitmq-data:
  postgres-data:
  redis-data: