Ctrl K
ComunidadGuías…

compose.yaml

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

Archivo original ↗Preguntar a KiBot
# Self-hosting — the "three commands" promise:
#   git clone https://github.com/Open-Incident/open-incident && cd open-incident
#   cp .env.example .env        # then fill in the secrets
#   docker compose up -d
#
# The application then answers on http://localhost:3000 and the status pages on
# http://<page>.status.localhost:3001 — unless BASE_DOMAIN / STATUS_BASE_DOMAIN
# in .env name your real domains.
# With SEED_DEMO=true (the .env.example default), the instance starts with the
# Skylark Systems demo workspace — login amelie@skylark.dev /
# demo-openincident — to be replaced by your own workspace from the first
# session on (`docker compose run --rm migrate pnpm workspace:create …`).
#
# The internal hosts (postgres, redis) are pinned here and take precedence over
# .env, written for development on the host machine's ports. Two database
# roles: the owner runs the migrations, the application role runs the product
# under row-level security — it must never be the same login.

name: open-incident-prod

services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-openincident}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-openincident}
      POSTGRES_DB: ${POSTGRES_DB:-openincident}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-openincident}"]
      interval: 5s
      timeout: 3s
      retries: 10

  redis:
    image: redis:8-alpine
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  migrate:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: migrate
    env_file:
      - .env
    environment:
      # Blanked on purpose. A .env copied from a development machine carries
      # NODE_EXTRA_CA_CERTS pointing at mkcert's authority on that machine, and
      # Node warns about the missing file on every line this container logs.
      # Development TLS is not a container's business.
      NODE_EXTRA_CA_CERTS: ""
      DATABASE_ADMIN_URL: postgres://${POSTGRES_USER:-openincident}:${POSTGRES_PASSWORD:-openincident}@postgres:5432/${POSTGRES_DB:-openincident}
      DATABASE_URL: postgres://openincident_app:${APP_DB_PASSWORD:-openincident_app}@postgres:5432/${POSTGRES_DB:-openincident}
      # Empty unless the telemetry profile is on; the column store's migrations
      # are skipped rather than failing when there is no store to migrate.
      CLICKHOUSE_URL: ${CLICKHOUSE_URL:-}
      # CLICKHOUSE_DATABASE here, CLICKHOUSE_DB on the store itself: the image's
      # entrypoint reads the second name and our client reads the first, so one
      # variable in .env feeds both rather than two that can drift apart.
      CLICKHOUSE_DATABASE: ${CLICKHOUSE_DB:-openincident}
      CLICKHOUSE_USER: ${CLICKHOUSE_USER:-openincident}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-openincident}
      APP_DB_PASSWORD: ${APP_DB_PASSWORD:-openincident_app}
      SEED_DEMO: ${SEED_DEMO:-true}
      BASE_DOMAIN: ${BASE_DOMAIN:-localhost:3000}
      STATUS_BASE_DOMAIN: ${STATUS_BASE_DOMAIN:-status.localhost:3001}
    depends_on:
      postgres:
        condition: service_healthy

  web:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: web
    env_file:
      - .env
    environment:
      # Blanked on purpose. A .env copied from a development machine carries
      # NODE_EXTRA_CA_CERTS pointing at mkcert's authority on that machine, and
      # Node warns about the missing file on every line this container logs.
      # Development TLS is not a container's business.
      NODE_EXTRA_CA_CERTS: ""
      DATABASE_URL: postgres://openincident_app:${APP_DB_PASSWORD:-openincident_app}@postgres:5432/${POSTGRES_DB:-openincident}
      REDIS_URL: redis://redis:6379
      # The public addresses: your domains in .env, the compose ports by default.
      BASE_DOMAIN: ${BASE_DOMAIN:-localhost:3000}
      STATUS_BASE_DOMAIN: ${STATUS_BASE_DOMAIN:-status.localhost:3001}
      # Empty unless the telemetry profile is on. Every telemetry screen checks
      # it and says the module is not installed rather than drawing nothing.
      CLICKHOUSE_URL: ${CLICKHOUSE_URL:-}
      # CLICKHOUSE_DATABASE here, CLICKHOUSE_DB on the store itself: the image's
      # entrypoint reads the second name and our client reads the first, so one
      # variable in .env feeds both rather than two that can drift apart.
      CLICKHOUSE_DATABASE: ${CLICKHOUSE_DB:-openincident}
      CLICKHOUSE_USER: ${CLICKHOUSE_USER:-openincident}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-openincident}
      # Where a collector reaches the ingestion endpoint, for the Connect
      # screen to print an address somebody can paste.
      TELEMETRY_PUBLIC_ORIGIN: ${TELEMETRY_PUBLIC_ORIGIN:-}
    ports:
      - "${WEB_PORT:-3000}:3000"
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy
    restart: unless-stopped

  # The public status pages: reads snapshots only, no Redis — stays up when
  # the product is down. Point status.<your domain> (and customers' CNAMEs) here.
  status:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: status
    env_file:
      - .env
    environment:
      # Blanked on purpose. A .env copied from a development machine carries
      # NODE_EXTRA_CA_CERTS pointing at mkcert's authority on that machine, and
      # Node warns about the missing file on every line this container logs.
      # Development TLS is not a container's business.
      NODE_EXTRA_CA_CERTS: ""
      DATABASE_URL: postgres://openincident_app:${APP_DB_PASSWORD:-openincident_app}@postgres:5432/${POSTGRES_DB:-openincident}
      BASE_DOMAIN: ${BASE_DOMAIN:-localhost:3000}
      STATUS_BASE_DOMAIN: ${STATUS_BASE_DOMAIN:-status.localhost:3001}
    ports:
      - "${STATUS_PORT:-3001}:3001"
    depends_on:
      migrate:
        condition: service_completed_successfully
    restart: unless-stopped

  # One worker serves every queue, and that is the default: an installation
  # must never need two containers to send an email. WORKER_QUEUES is what
  # changes that — see the `worker-bulk` service below, and .env.example.
  worker:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: worker
    env_file:
      - .env
    environment:
      # Blanked on purpose. A .env copied from a development machine carries
      # NODE_EXTRA_CA_CERTS pointing at mkcert's authority on that machine, and
      # Node warns about the missing file on every line this container logs.
      # Development TLS is not a container's business.
      NODE_EXTRA_CA_CERTS: ""
      DATABASE_URL: postgres://openincident_app:${APP_DB_PASSWORD:-openincident_app}@postgres:5432/${POSTGRES_DB:-openincident}
      REDIS_URL: redis://redis:6379
      INTERNAL_WEB_ORIGIN: http://web:3000
      BASE_DOMAIN: ${BASE_DOMAIN:-localhost:3000}
      STATUS_BASE_DOMAIN: ${STATUS_BASE_DOMAIN:-status.localhost:3001}
      WORKER_QUEUES: ${WORKER_QUEUES:-}
      WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-}
      # Empty unless the telemetry profile is on. The sweeps that read the
      # column store — telemetry monitors, the service map, exception
      # regressions, the SLOs — all check for it and do nothing without one.
      CLICKHOUSE_URL: ${CLICKHOUSE_URL:-}
      # CLICKHOUSE_DATABASE here, CLICKHOUSE_DB on the store itself: the image's
      # entrypoint reads the second name and our client reads the first, so one
      # variable in .env feeds both rather than two that can drift apart.
      CLICKHOUSE_DATABASE: ${CLICKHOUSE_DB:-openincident}
      CLICKHOUSE_USER: ${CLICKHOUSE_USER:-openincident}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-openincident}
    # The ping monitor sends ICMP from an unprivileged datagram socket. Docker
    # has set this range in every container since 2020, so this line changes
    # nothing today — it is written down so a future default cannot take the
    # feature away without anyone noticing.
    sysctls:
      net.ipv4.ping_group_range: "0 2147483647"
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy
    restart: unless-stopped

  # A second worker for the heavy work — off by default, on purpose.
  #
  # An installation only needs it once the background work starts crowding
  # itself out: the fan-out to a status page's subscribers, the purges, the
  # nightly syncs. Starting it means giving the first worker the other half,
  # otherwise both serve everything and nothing is separated:
  #   WORKER_QUEUES=urgent docker compose --profile split up -d worker worker-bulk
  # The image is the same one; only the variable differs. A single process
  # remains the default, and it is the one `docker compose up` starts.
  worker-bulk:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: worker
    profiles: ["split"]
    env_file:
      - .env
    environment:
      # Blanked on purpose. A .env copied from a development machine carries
      # NODE_EXTRA_CA_CERTS pointing at mkcert's authority on that machine, and
      # Node warns about the missing file on every line this container logs.
      # Development TLS is not a container's business.
      NODE_EXTRA_CA_CERTS: ""
      DATABASE_URL: postgres://openincident_app:${APP_DB_PASSWORD:-openincident_app}@postgres:5432/${POSTGRES_DB:-openincident}
      REDIS_URL: redis://redis:6379
      INTERNAL_WEB_ORIGIN: http://web:3000
      BASE_DOMAIN: ${BASE_DOMAIN:-localhost:3000}
      STATUS_BASE_DOMAIN: ${STATUS_BASE_DOMAIN:-status.localhost:3001}
      WORKER_QUEUES: bulk
      CLICKHOUSE_URL: ${CLICKHOUSE_URL:-}
      # CLICKHOUSE_DATABASE here, CLICKHOUSE_DB on the store itself: the image's
      # entrypoint reads the second name and our client reads the first, so one
      # variable in .env feeds both rather than two that can drift apart.
      CLICKHOUSE_DATABASE: ${CLICKHOUSE_DB:-openincident}
      CLICKHOUSE_USER: ${CLICKHOUSE_USER:-openincident}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-openincident}
      WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-}
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy
    restart: unless-stopped

  # The browser runner for synthetic monitors — off by default, on purpose.
  # Its image carries Chromium (~400 MB) and a run costs several hundred
  # megabytes of memory, so an ordinary `docker compose up` never pulls it:
  #   docker compose --profile synthetic up -d synthetic
  # While it runs it keeps a key alive in Redis, and that is what makes the
  # synthetic type selectable in the product. Stop it and the type goes back to
  # greyed, with this command as its reason.
  synthetic:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: synthetic
    profiles: ["synthetic"]
    env_file:
      - .env
    environment:
      # Blanked on purpose. A .env copied from a development machine carries
      # NODE_EXTRA_CA_CERTS pointing at mkcert's authority on that machine, and
      # Node warns about the missing file on every line this container logs.
      # Development TLS is not a container's business.
      NODE_EXTRA_CA_CERTS: ""
      DATABASE_URL: postgres://openincident_app:${APP_DB_PASSWORD:-openincident_app}@postgres:5432/${POSTGRES_DB:-openincident}
      REDIS_URL: redis://redis:6379
      INTERNAL_WEB_ORIGIN: http://web:3000
      BASE_DOMAIN: ${BASE_DOMAIN:-localhost:3000}
      STATUS_BASE_DOMAIN: ${STATUS_BASE_DOMAIN:-status.localhost:3001}
      # How many journeys play at once. Each is a Chromium context: raising it
      # on a one-CPU host makes every monitor report the queue's latency
      # instead of the site's.
      SYNTHETIC_CONCURRENCY: ${SYNTHETIC_CONCURRENCY:-2}
    # Chromium writes its shared memory to /dev/shm, and Docker's default 64 MB
    # is not enough for a real page: the browser dies mid-journey and the
    # monitor reports an outage that is ours, not the site's.
    shm_size: "1gb"
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy
    restart: unless-stopped

  # ---------- Observability (spec 15), behind the `telemetry` profile ----------
  #
  # Off by default, and that is a choice rather than an omission. A column
  # store is a second database to run, back up and watch, and the incident
  # product is whole without one: an installation that only wants alerting and
  # on-call should not be paying for ClickHouse.
  #
  # Turning it on is one command and two variables:
  #   docker compose --profile telemetry up -d
  # with CLICKHOUSE_URL set in .env so the web app, the worker and the
  # migrations all know the store exists. Without it every telemetry screen
  # says so and offers this line, rather than drawing empty charts.
  clickhouse:
    image: clickhouse/clickhouse-server:25.3-alpine
    profiles: ["telemetry"]
    environment:
      CLICKHOUSE_DB: ${CLICKHOUSE_DB:-openincident}
      CLICKHOUSE_USER: ${CLICKHOUSE_USER:-openincident}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-openincident}
      # Without this the entrypoint creates the user with no password at all,
      # and ClickHouse then refuses every authenticated connection.
      CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1"
    volumes:
      - chdata:/var/lib/clickhouse
    # ClickHouse opens a file per part and merges them constantly; the default
    # 1024 is reached by a single busy table and the symptom is a store that
    # stops accepting writes.
    ulimits:
      nofile: { soft: 262144, hard: 262144 }
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8123/ping"]
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped

  # The OTLP endpoint your services send to. Its own container because it is
  # the one part of the stack that faces the outside on a port of its own, and
  # because a busy installation will want more than one of it without
  # multiplying the web app.
  telemetry:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: telemetry
    profiles: ["telemetry"]
    env_file:
      - .env
    environment:
      # Blanked on purpose. A .env copied from a development machine carries
      # NODE_EXTRA_CA_CERTS pointing at mkcert's authority on that machine, and
      # Node warns about the missing file on every line this container logs.
      # Development TLS is not a container's business.
      NODE_EXTRA_CA_CERTS: ""
      DATABASE_URL: postgres://openincident_app:${APP_DB_PASSWORD:-openincident_app}@postgres:5432/${POSTGRES_DB:-openincident}
      CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
      CLICKHOUSE_DATABASE: ${CLICKHOUSE_DB:-openincident}
      CLICKHOUSE_USER: ${CLICKHOUSE_USER:-openincident}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-openincident}
      TELEMETRY_PORT: "4318"
    ports:
      # 4318 is OTLP/HTTP's registered port, so a collector pointed at this
      # host needs no port in its endpoint.
      - "${TELEMETRY_PORT:-4318}:4318"
    depends_on:
      migrate:
        condition: service_completed_successfully
      clickhouse:
        condition: service_healthy
    restart: unless-stopped

  # Optional object storage for workspace files (logos). Start it with
  # `docker compose --profile storage up -d` and set S3_* in .env accordingly,
  # or point S3_* at any S3-compatible bucket you already run.
  minio:
    # quay.io, not Docker Hub: minio/minio there stopped being pullable in
    # September 2026 ("repository does not exist"), and the nightly smoke ran
    # into it four nights running before anyone noticed. Pinned to a release
    # rather than latest for the same reason — a tag that moves under a
    # scheduled job is a job that fails on someone else's schedule.
    image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
    profiles: ["storage"]
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:-openincident}
      MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:-openincident}
    volumes:
      - miniodata:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped

  minio-init:
    # The client publishes no release tags on quay, so it is pinned by digest.
    image: quay.io/minio/mc@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727
    profiles: ["storage"]
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      /bin/sh -c "
      mc alias set local http://minio:9000 ${S3_ACCESS_KEY_ID:-openincident} ${S3_SECRET_ACCESS_KEY:-openincident} &&
      mc mb --ignore-existing local/${S3_BUCKET:-openincident} &&
      echo bucket ready"

volumes:
  chdata:
  pgdata:
  redisdata:
  miniodata: