Sign inSign up

kahmyl/scry-stack:latest

Manifest digest

sha256:3d71aab3eabad7064998fd2b39803b6d3054daec956434ce76899e466a4c470c

Last pushed

18 days by kahmyl

Type

Compose

Manifest digest

sha256:3d71aab3eabad7064998fd2b39803b6d3054daec956434ce76899e466a4c470c

Compose file content

name: scry

x-api-environment: &api-environment
  NODE_ENV: production
  DATABASE_URL: postgres://scry:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@postgres:5432/${SCRY_DATABASE_NAME:-scry_browser_v2}
  REDIS_URL: redis://redis:6379
  BROWSER_V2_ARTIFACT_ROOT: /workspace/apps/api/artifacts/browser-v2
  SUPABASE_URL: ${SUPABASE_URL:?Set SUPABASE_URL}
  SCRY_SERVICE_TOKEN: ${SCRY_SERVICE_TOKEN:?Set SCRY_SERVICE_TOKEN}
  BROWSER_V2_ENABLED: "true"
  BROWSER_V2_RENDER_VIDEO: "true"
  BROWSER_V2_REQUIRE_ACTIVATED_CUTOVER: ${BROWSER_V2_REQUIRE_ACTIVATED_CUTOVER:-true}
  BROWSER_V2_PROTECTED_KEYS_JSON: ${BROWSER_V2_PROTECTED_KEYS_JSON:?Load the versioned browser keyring from the production secret manager}
  BROWSER_V2_PROTECTED_KEY_VERSION: ${BROWSER_V2_PROTECTED_KEY_VERSION:?Set the active browser key version}

x-backup-environment: &backup-environment
  RESTIC_REPOSITORY: ${RESTIC_REPOSITORY:-}
  RESTIC_PASSWORD: ${RESTIC_PASSWORD:-}
  AWS_ACCESS_KEY_ID: ${BACKUP_AWS_ACCESS_KEY_ID:-}
  AWS_SECRET_ACCESS_KEY: ${BACKUP_AWS_SECRET_ACCESS_KEY:-}
  AWS_DEFAULT_REGION: ${BACKUP_AWS_REGION:-}
  AWS_ENDPOINT: ${BACKUP_AWS_ENDPOINT:-}
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
  PGHOST: postgres
  PGUSER: scry
  BACKUP_KEEP_HOURLY: ${BACKUP_KEEP_HOURLY:-48}
  BACKUP_KEEP_DAILY: ${BACKUP_KEEP_DAILY:-30}
  BACKUP_KEEP_WEEKLY: ${BACKUP_KEEP_WEEKLY:-12}
  BACKUP_KEEP_MONTHLY: ${BACKUP_KEEP_MONTHLY:-12}
  RESTORE_DRILL_RTO_SECONDS: ${RESTORE_DRILL_RTO_SECONDS:-3600}

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    environment:
      SCRY_DOMAIN: ${SCRY_DOMAIN:?Set SCRY_DOMAIN}
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    configs:
      - source: caddyfile
        target: /etc/caddy/Caddyfile
    volumes:
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      web:
        condition: service_healthy
      api:
        condition: service_healthy
      mcp:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:2019/config/"]
      start_period: 10s
      start_interval: 1s
      interval: 5s
      timeout: 3s
      retries: 20

  web:
    image: ${SCRY_IMAGE_REF:?Set SCRY_IMAGE_REF to an immutable image digest}
    restart: unless-stopped
    command: ["node", "apps/web/scripts/serve-dist.mjs"]
    environment:
      WEB_PORT: "3000"
      SCRY_PUBLIC_API_BASE_URL: /api
      SCRY_PUBLIC_MCP_SERVER_URL: /mcp
      SUPABASE_URL: ${SUPABASE_URL:?Set SUPABASE_URL}
      SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:?Set SUPABASE_PUBLISHABLE_KEY}
      SCRY_RELEASE_ID: ${SCRY_RELEASE_ID:?Set SCRY_RELEASE_ID}
      SCRY_SCHEMA_FINGERPRINT: ${SCRY_SCHEMA_FINGERPRINT:?Set SCRY_SCHEMA_FINGERPRINT}
    depends_on:
      api:
        condition: service_started
      mcp:
        condition: service_started
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
      start_period: 10s
      start_interval: 1s
      interval: 5s
      timeout: 3s
      retries: 20

  api:
    image: ${SCRY_IMAGE_REF:?Set SCRY_IMAGE_REF to an immutable image digest}
    restart: unless-stopped
    environment:
      <<: *api-environment
      API_HOST: 0.0.0.0
      API_PORT: "4000"
      API_CORS_ORIGINS: https://${SCRY_DOMAIN:?Set SCRY_DOMAIN}
      SCRY_RELEASE_ID: ${SCRY_RELEASE_ID:?Set SCRY_RELEASE_ID}
      SCRY_SCHEMA_FINGERPRINT: ${SCRY_SCHEMA_FINGERPRINT:?Set SCRY_SCHEMA_FINGERPRINT}
    command:
      [
        "apps/api/node_modules/.bin/tsx",
        "--tsconfig",
        "apps/api/tsconfig.json",
        "apps/api/src/browser-v2-main.ts",
      ]
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy
    volumes:
      - artifacts:/workspace/apps/api/artifacts/browser-v2
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4000/api/v2/health"]
      start_period: 20s
      start_interval: 1s
      interval: 5s
      timeout: 3s
      retries: 20

  worker:
    image: ${SCRY_IMAGE_REF:?Set SCRY_IMAGE_REF to an immutable image digest}
    restart: unless-stopped
    command:
      [
        "apps/api/node_modules/.bin/tsx",
        "--tsconfig",
        "apps/api/tsconfig.json",
        "apps/api/src/browser-v2-worker.ts",
      ]
    environment:
      <<: *api-environment
      SCRY_BROWSER_CHANNEL: chromium
      BROWSER_V2_SESSION_CONCURRENCY: ${SCRY_WORKER_CONCURRENCY:-4}
      BROWSER_V2_COMMAND_CONCURRENCY: ${SCRY_COMMAND_CONCURRENCY:-8}
      SCRY_RELEASE_ID: ${SCRY_RELEASE_ID:?Set SCRY_RELEASE_ID}
      SCRY_SCHEMA_FINGERPRINT: ${SCRY_SCHEMA_FINGERPRINT:?Set SCRY_SCHEMA_FINGERPRINT}
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy
    volumes:
      - artifacts:/workspace/apps/api/artifacts/browser-v2
    shm_size: 1gb
    stop_grace_period: 30s

  migrate:
    image: ${SCRY_IMAGE_REF:?Set SCRY_IMAGE_REF to an immutable image digest}
    restart: "no"
    command:
      [
        "apps/api/node_modules/.bin/tsx",
        "--tsconfig",
        "apps/api/tsconfig.json",
        "apps/api/scripts/migrate-browser-v2.ts",
      ]
    environment:
      BROWSER_V2_DATABASE_URL: postgres://scry:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@postgres:5432/${SCRY_DATABASE_NAME:-scry_browser_v2}
      BROWSER_V2_EXPECTED_DATABASE: ${SCRY_DATABASE_NAME:-scry_browser_v2}
      BROWSER_V2_SCHEMA_CONFIRM: CREATE_EMPTY_V2
    depends_on:
      postgres:
        condition: service_healthy

  mcp:
    image: ${SCRY_IMAGE_REF:?Set SCRY_IMAGE_REF to an immutable image digest}
    restart: unless-stopped
    command:
      [
        "apps/mcp/node_modules/.bin/tsx",
        "--tsconfig",
        "apps/mcp/tsconfig.json",
        "apps/mcp/src/browser-v2-http.ts",
      ]
    environment:
      MCP_HOST: 0.0.0.0
      MCP_PORT: "4100"
      MCP_ALLOWED_ORIGINS: https://${SCRY_DOMAIN:?Set SCRY_DOMAIN}
      SCRY_API_BASE_URL: http://api:4000/api
      SCRY_PUBLIC_API_BASE_URL: https://${SCRY_DOMAIN:?Set SCRY_DOMAIN}/api
      SCRY_SERVICE_TOKEN: ${SCRY_SERVICE_TOKEN:?Set SCRY_SERVICE_TOKEN}
      SCRY_RELEASE_ID: ${SCRY_RELEASE_ID:?Set SCRY_RELEASE_ID}
      SCRY_SCHEMA_FINGERPRINT: ${SCRY_SCHEMA_FINGERPRINT:?Set SCRY_SCHEMA_FINGERPRINT}
    depends_on:
      api:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4100/health"]
      start_period: 20s
      start_interval: 1s
      interval: 5s
      timeout: 3s
      retries: 20

  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${SCRY_DATABASE_NAME:-scry_browser_v2}
      POSTGRES_USER: scry
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - postgres_wal_archive:/wal-archive
    command:
      - postgres
      - -c
      - wal_level=replica
      - -c
      - archive_mode=${POSTGRES_ARCHIVE_MODE:-off}
      - -c
      - archive_timeout=${POSTGRES_ARCHIVE_TIMEOUT:-300s}
      - -c
      - hba_file=/etc/postgresql/pg_hba.conf
      - -c
      - archive_command=test -f /wal-archive/%f || (cp %p /wal-archive/.%f.tmp && chmod 0600 /wal-archive/.%f.tmp && mv /wal-archive/.%f.tmp /wal-archive/%f)
    configs:
      - source: postgres_hba
        target: /etc/postgresql/pg_hba.conf
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U scry -d ${SCRY_DATABASE_NAME:-scry_browser_v2}"]
      start_period: 10s
      start_interval: 1s
      interval: 5s
      timeout: 5s
      retries: 10
    depends_on:
      postgres-storage-init:
        condition: service_completed_successfully

  postgres-storage-init:
    image: postgres:17-alpine
    restart: "no"
    user: root
    command: ["sh", "-c", "chown -R postgres:postgres /wal-archive && chmod 0700 /wal-archive"]
    volumes:
      - postgres_wal_archive:/wal-archive

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes", "--save", "60", "1"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      start_period: 10s
      start_interval: 1s
      interval: 5s
      timeout: 3s
      retries: 10

  postgres-base-backup:
    profiles: [operations]
    image: ${SCRY_BACKUP_IMAGE_REF:-scry-backup:local}
    restart: unless-stopped
    command: ["postgres-base"]
    environment:
      <<: *backup-environment
      POSTGRES_BASE_BACKUP_INTERVAL_SECONDS: ${POSTGRES_BASE_BACKUP_INTERVAL_SECONDS:-86400}
    volumes:
      - backup_staging:/staging
      - backup_metrics:/metrics
    depends_on:
      backup-init:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy

  postgres-wal-backup:
    profiles: [operations]
    image: ${SCRY_BACKUP_IMAGE_REF:-scry-backup:local}
    restart: unless-stopped
    command: ["postgres-wal"]
    environment:
      <<: *backup-environment
      POSTGRES_WAL_BACKUP_INTERVAL_SECONDS: ${POSTGRES_WAL_BACKUP_INTERVAL_SECONDS:-300}
    volumes:
      - postgres_wal_archive:/wal-archive:ro
      - backup_metrics:/metrics
    depends_on:
      backup-init:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy

  artifact-backup:
    profiles: [operations]
    image: ${SCRY_BACKUP_IMAGE_REF:-scry-backup:local}
    restart: unless-stopped
    command: ["artifacts"]
    environment:
      <<: *backup-environment
      ARTIFACT_BACKUP_INTERVAL_SECONDS: ${ARTIFACT_BACKUP_INTERVAL_SECONDS:-900}
    volumes:
      - artifacts:/artifacts:ro
      - backup_metrics:/metrics
    depends_on:
      backup-init:
        condition: service_completed_successfully

  backup-maintenance:
    profiles: [operations]
    image: ${SCRY_BACKUP_IMAGE_REF:-scry-backup:local}
    restart: unless-stopped
    command: ["maintenance"]
    environment:
      <<: *backup-environment
      BACKUP_MAINTENANCE_INTERVAL_SECONDS: ${BACKUP_MAINTENANCE_INTERVAL_SECONDS:-86400}
      BACKUP_CHECK_DATA_SUBSET: ${BACKUP_CHECK_DATA_SUBSET:-5%}
    volumes:
      - backup_metrics:/metrics
    depends_on:
      backup-init:
        condition: service_completed_successfully

  restore-drill:
    profiles: [operations]
    image: ${SCRY_BACKUP_IMAGE_REF:-scry-backup:local}
    restart: unless-stopped
    command: ["restore-drill"]
    environment:
      <<: *backup-environment
      RESTORE_DRILL_INTERVAL_SECONDS: ${RESTORE_DRILL_INTERVAL_SECONDS:-604800}
    volumes:
      - restore_drill:/restore-drill
      - backup_metrics:/metrics
    depends_on:
      backup-init:
        condition: service_completed_successfully

  backup-init:
    profiles: [operations]
    image: ${SCRY_BACKUP_IMAGE_REF:-scry-backup:local}
    restart: "no"
    command: ["init"]
    environment:
      <<: *backup-environment

  postgres-exporter:
    profiles: [operations]
    image: quay.io/prometheuscommunity/postgres-exporter:v0.17.1
    restart: unless-stopped
    environment:
      DATA_SOURCE_NAME: postgresql://scry:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@postgres:5432/${SCRY_DATABASE_NAME:-scry_browser_v2}?sslmode=disable
    depends_on:
      postgres:
        condition: service_healthy

  redis-exporter:
    profiles: [operations]
    image: oliver006/redis_exporter:v1.67.0
    restart: unless-stopped
    environment:
      REDIS_ADDR: redis://redis:6379
    depends_on:
      redis:
        condition: service_healthy

  blackbox-exporter:
    profiles: [operations]
    image: prom/blackbox-exporter:v0.27.0
    restart: unless-stopped
    command: ["--config.file=/etc/blackbox/blackbox.yml"]
    configs:
      - source: blackbox_config
        target: /etc/blackbox/blackbox.yml

  backup-metrics:
    profiles: [operations]
    image: prom/node-exporter:v1.9.1
    restart: unless-stopped
    command:
      [
        "--collector.disable-defaults",
        "--collector.textfile",
        "--collector.textfile.directory=/metrics",
      ]
    volumes:
      - backup_metrics:/metrics:ro

  alertmanager:
    profiles: [operations]
    image: prom/alertmanager:v0.28.1
    restart: unless-stopped
    command: ["--config.file=/etc/alertmanager/alertmanager.yml", "--storage.path=/alertmanager"]
    configs:
      - source: alertmanager_config
        target: /etc/alertmanager/alertmanager.yml
    volumes:
      - alertmanager_data:/alertmanager

  prometheus:
    profiles: [operations]
    image: prom/prometheus:v3.5.0
    restart: unless-stopped
    command:
      [
        "--config.file=/etc/prometheus/prometheus.yml",
        "--storage.tsdb.path=/prometheus",
        "--storage.tsdb.retention.time=30d",
      ]
    configs:
      - source: prometheus_config
        target: /etc/prometheus/prometheus.yml
      - source: prometheus_rules
        target: /etc/prometheus/rules.yml
    volumes:
      - prometheus_data:/prometheus
    depends_on:
      - alertmanager
      - postgres-exporter
      - redis-exporter
      - blackbox-exporter
      - backup-metrics

volumes:
  postgres_data:
  redis_data:
  artifacts:
  postgres_wal_archive:
  backup_staging:
  backup_metrics:
  restore_drill:
  prometheus_data:
  alertmanager_data:
  caddy_data:
  caddy_config:

configs:
  postgres_hba:
    content: |
      # Unix-socket access stays inside the PostgreSQL container.
      local   all             all                                     trust
      # Every network client, including backup clients, must authenticate.
      host    all             all             0.0.0.0/0               scram-sha-256
      host    all             all             ::/0                    scram-sha-256
      host    replication     all             0.0.0.0/0               scram-sha-256
      host    replication     all             ::/0                    scram-sha-256
  prometheus_config:
    content: |
      global:
        scrape_interval: 15s
        evaluation_interval: 15s
      
      rule_files:
        - /etc/prometheus/rules.yml
      
      alerting:
        alertmanagers:
          - static_configs:
              - targets: ["alertmanager:9093"]
      
      scrape_configs:
        - job_name: prometheus
          static_configs:
            - targets: ["prometheus:9090"]
        - job_name: postgres
          static_configs:
            - targets: ["postgres-exporter:9187"]
        - job_name: redis
          static_configs:
            - targets: ["redis-exporter:9121"]
        - job_name: backup
          static_configs:
            - targets: ["backup-metrics:9100"]
        - job_name: scry-blackbox
          metrics_path: /probe
          params:
            module: [http_2xx]
          static_configs:
            - targets:
                - http://api:4000/api/ready
                - http://mcp:4100/health
                - http://web:3000/health
          relabel_configs:
            - source_labels: [__address__]
              target_label: __param_target
            - source_labels: [__param_target]
              target_label: instance
            - target_label: __address__
              replacement: blackbox-exporter:9115
  prometheus_rules:
    content: |
      groups:
        - name: scry-production
          rules:
            - alert: ScryEndpointUnavailable
              expr: probe_success{job="scry-blackbox"} == 0
              for: 2m
              labels: { severity: critical }
              annotations:
                summary: "Scry endpoint unavailable: {{ $$labels.instance }}"
            - alert: PostgreSQLUnavailable
              expr: pg_up == 0
              for: 1m
              labels: { severity: critical }
              annotations: { summary: "PostgreSQL exporter cannot reach PostgreSQL" }
            - alert: RedisUnavailable
              expr: redis_up == 0
              for: 1m
              labels: { severity: critical }
              annotations: { summary: "Redis exporter cannot reach Redis" }
            - alert: BackupJobFailed
              expr: scry_backup_last_run_status == 0
              for: 5m
              labels: { severity: critical }
              annotations: { summary: "Backup or restore operation failed: {{ $$labels.job }}" }
            - alert: PostgreSQLWalBackupStale
              expr: time() - scry_backup_last_success_unixtime{job="postgres-wal"} > 600
              for: 2m
              labels: { severity: critical }
              annotations:
                { summary: "PostgreSQL WAL off-host backup exceeds the 10-minute alert threshold" }
            - alert: ArtifactBackupStale
              expr: time() - scry_backup_last_success_unixtime{job="artifacts"} > 1800
              for: 5m
              labels: { severity: warning }
              annotations: { summary: "Artifact backup is older than 30 minutes" }
            - alert: RestoreDrillStale
              expr: time() - scry_backup_last_success_unixtime{job="restore-drill"} > 691200
              for: 1h
              labels: { severity: critical }
              annotations: { summary: "No successful restore drill in eight days" }
  blackbox_config:
    content: |
      modules:
        http_2xx:
          prober: http
          timeout: 5s
          http:
            method: GET
            preferred_ip_protocol: ip4
  alertmanager_config:
    content: |
      route:
        receiver: operations
        group_by: [alertname, severity]
        group_wait: 30s
        group_interval: 5m
        repeat_interval: 4h
      receivers:
        - name: operations
          webhook_configs:
            - url: ${SCRY_ALERT_WEBHOOK_URL:-http://127.0.0.1:65535/operations-disabled}
              send_resolved: true
  caddyfile:
    content: |
      ${SCRY_DOMAIN:?Set SCRY_DOMAIN} {
        handle /api/* {
          reverse_proxy api:4000
        }
        handle /mcp {
          reverse_proxy mcp:4100
        }
        handle {
          reverse_proxy web:3000
        }
        encode zstd gzip
      }

Docker commands

docker compose -f oci://kahmyl/scry-stack:latest up

Use the above command to pull and run the Compose file. Learn more⁠.

Images used

Image + 1 more

The PostgreSQL object-relational database system provides reliability and data integrity.


Pulls

1B+

Stars

15023

Last Updated

2 days

Image + 1 more

Redis is the world’s fastest data platform for caching, vector search, and NoSQL databases.


Pulls

1B+

Stars

13619

Last Updated

2 days

Image

Caddy 2 is a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go.


Pulls

500M+

Stars

988

Last Updated

1 day

Image

prom/node-exporter


Pulls

1B+

Stars

418

Last Updated

2 days

Image

prom/prometheus


Pulls

1B+

Stars

2075

Last Updated

2 days

Image

prom/alertmanager


Pulls

100M+

Stars

252

Last Updated

about 20 hours

Image + 1 more

prom/blackbox-exporter


Pulls

100M+

Stars

110

Last Updated

1 day

Image

Prometheus Exporter for Redis Metrics. Supports Redis 2.x, 3.x, 4.x and 5.x


Pulls

100M+

Stars

54

Last Updated

4 days

Image

prometheuscommunity/postgres-exporter


Pulls

5M+

Stars

19

Last Updated

4 days