services:
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    networks: [tngl]

  postgres:
    image: postgres:14-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: tnglr
      POSTGRES_PASSWORD: tnglr
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./localinfra/postgres-init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U tnglr -d plc"]
      interval: 2s
      timeout: 2s
      retries: 30
      start_period: 5s
    networks: [tngl]

  pds:
    image: ghcr.io/bluesky-social/pds:0.4.219
    restart: unless-stopped
    env_file: localinfra/pds.env
    environment:
      PDS_DID_PLC_URL: http://plc:8080
      NODE_EXTRA_CA_CERTS: /caddy-ca/root.crt
    volumes:
      - pds-data:/pds
      - ./localinfra/certs/root.crt:/caddy-ca/root.crt:ro
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/xrpc/_health"]
      interval: 2s
      timeout: 2s
      retries: 30
      start_period: 5s
    depends_on:
      plc:
        condition: service_started
    networks: [tngl]

  init-accounts:
    image: alpine:3.22
    restart: "no"
    env_file: localinfra/pds.env
    environment:
      PDS_URL: http://pds:3000
      OWNER_USER: alice
      KNOT_HOSTNAME: knot.tngl.boltless.dev
      SPINDLE_HOSTNAME: spindle.tngl.boltless.dev
    volumes:
      - ./localinfra/scripts/init-accounts.sh:/init.sh:ro
      - init-state:/shared
    command: sh -c "apk add --no-cache curl jq >/dev/null && sh /init.sh"
    depends_on:
      pds:
        condition: service_healthy
    networks: [tngl]

  plc:
    platform: linux/amd64
    image: ghcr.io/bluesky-social/did-method-plc:plc-f2ab7516bac5bc0f3f86842fa94e996bd1b3815b
    restart: unless-stopped
    environment:
      DEBUG_MODE: "1"
      LOG_ENABLED: "true"
      LOG_LEVEL: debug
      LOG_DESTINATION: "1"
      PLC_VERSION: 0.0.1
      PORT: "8080"
      DB_CREDS_JSON: &DB_CREDS_JSON '{"username":"tnglr","password":"tnglr","host":"postgres","port":5432}'
      DB_MIGRATE_CREDS_JSON: *DB_CREDS_JSON
    depends_on: [postgres]
    networks: [tngl]

  jetstream:
    platform: linux/amd64
    image: ghcr.io/bluesky-social/jetstream:sha-d5a3b62
    restart: unless-stopped
    environment:
      JETSTREAM_DATA_DIR: /data
      JETSTREAM_LIVENESS_TTL: 300s
      JETSTREAM_WS_URL: wss://pds.tngl.boltless.dev/xrpc/com.atproto.sync.subscribeRepos
    volumes:
      - jetstream-data:/data
      - ./localinfra/certs/root.crt:/etc/ssl/certs/ca-certificates.crt:ro
    depends_on:
      pds:
        condition: service_healthy
    networks: [tngl]

  knot:
    build:
      context: .
      dockerfile: localinfra/knot.Dockerfile
    restart: unless-stopped
    environment:
      KNOT_SERVER_HOSTNAME: knot.tngl.boltless.dev
      KNOT_SERVER_LISTEN_ADDR: 0.0.0.0:5555
      KNOT_SERVER_INTERNAL_LISTEN_ADDR: 127.0.0.1:5444
      KNOT_SERVER_DB_PATH: /home/git/knotserver.db
      KNOT_SERVER_PLC_URL: https://plc.tngl.boltless.dev
      KNOT_SERVER_JETSTREAM_ENDPOINT: wss://jetstream.tngl.boltless.dev/subscribe
      KNOT_SERVER_DEV: "false"
      KNOT_REPO_SCAN_PATH: /home/git/repositories
      APPVIEW_ENDPOINT: https://tangled.org
      KNOT_MIRRORS: https://mirror.tngl.boltless.dev
    ports:
      - "2222:22"
    volumes:
      - knot-data:/home/git
      - knot-ssh-keys:/etc/ssh/keys
      - init-state:/shared:ro
      - ./localinfra/certs/root.crt:/usr/local/share/ca-certificates/caddy.crt:ro
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:5555/"]
      interval: 2s
      timeout: 2s
      retries: 60
      start_period: 30s
    depends_on:
      plc:
        condition: service_started
      jetstream:
        condition: service_started
      knotmirror:
        condition: service_healthy
      init-accounts:
        condition: service_completed_successfully
    networks: [tngl]

  ncps-migrate:
    image: &ncps-image ghcr.io/kalbasit/ncps:v0.9.4
    profiles: ["linux"]
    restart: "no"
    command:
      - /bin/dbmate
      - --migrations-dir=/share/ncps/db/migrations/sqlite
      - --no-dump-schema
      - --url=sqlite:/storage/db.sqlite
      - up
    volumes:
      - ncps-data:/storage
    networks: [tngl]

  ncps:
    image: *ncps-image
    profiles: ["linux"]
    restart: unless-stopped
    environment:
      ANALYTICS_REPORTING_ENABLED: "false"
    command: ["/bin/ncps", "serve", "--config", "/etc/ncps/ncps.yaml"]
    volumes:
      - ncps-data:/storage
      - ./localinfra/ncps.yaml:/etc/ncps/ncps.yaml:ro
      - ./localinfra/ncps-secret-key:/keys/ncps-secret-key:ro
    depends_on:
      ncps-migrate:
        condition: service_completed_successfully
    networks: [tngl]

  spindle:
    profiles: ["linux"]
    build:
      context: .
      dockerfile: localinfra/spindle.Dockerfile
    restart: unless-stopped
    environment:
      SPINDLE_SERVER_HOSTNAME: spindle.tngl.boltless.dev
      SPINDLE_SERVER_LISTEN_ADDR: 0.0.0.0:6555
      SPINDLE_SERVER_DB_PATH: /var/lib/spindle/spindle.db
      SPINDLE_SERVER_PLC_URL: https://plc.tngl.boltless.dev
      SPINDLE_SERVER_JETSTREAM_ENDPOINT: wss://jetstream.tngl.boltless.dev/subscribe
      SPINDLE_SERVER_DEV: "true"
      SPINDLE_SERVER_DEV_EXTRA_HOSTS: knot.tngl.boltless.dev,mirror.tngl.boltless.dev
      SPINDLE_SERVER_TAP_DB_PATH: /var/lib/spindle/tap.db
      SPINDLE_SERVER_TAP_RELAY_URL: https://pds.tngl.boltless.dev
      SPINDLE_MICROVM_PIPELINES_IMAGE_DIR: /var/lib/spindle/images
      SPINDLE_MICROVM_PIPELINES_OVERLAY_DIR: /var/lib/spindle/overlays
      SPINDLE_MICROVM_PIPELINES_AGENT_PORT: "11240"
      SPINDLE_S3_LOG_BUCKET: ""
      SPINDLE_MICROVM_PIPELINES_ENABLE_CGROUPS: "false"
      # route guest nix substitution + uploads through the local ncps cache.
      # ncps re-signs on serve with cache.local's key, so the guest trusts the
      # matching public key below (no signing happens in spindle itself).
      SPINDLE_NIX_CACHE_READ_URLS: http://ncps:8501
      SPINDLE_NIX_CACHE_TRUSTED_PUBLIC_KEYS: cache.local:F7YqpMzuBdILYd/v+wMZN2YKxCzliXQyFmeezOxw7rU=
      SPINDLE_NIX_CACHE_UPLOAD_URL: http://ncps:8501/upload
    # these two are required for cgroups, uncomment if testing
    # privileged: true
    # cgroup: host
    devices:
      - /dev/vsock:/dev/vsock
      - /dev/kvm:/dev/kvm
      - /dev/vhost-vsock:/dev/vhost-vsock
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
    security_opt:
      - label=disable
      - seccomp=unconfined
    volumes:
      - spindle-data:/var/lib/spindle
      - spindle-logs:/var/log/spindle
      - ./out/localinfra-spindle-images:/var/lib/spindle/images:ro
      - init-state:/shared:ro
      - ./localinfra/certs/root.crt:/usr/local/share/ca-certificates/caddy.crt:ro
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:6555/"]
      interval: 2s
      timeout: 2s
      retries: 30
      start_period: 5s
    depends_on:
      plc:
        condition: service_started
      jetstream:
        condition: service_started
      init-accounts:
        condition: service_completed_successfully
      ncps:
        condition: service_started
    networks: [tngl]

  knotmirror-tap:
    image: ghcr.io/bluesky-social/indigo/tap:sha-4f47add43060c27e8a37d9d76482ecddf001fcd8 # 0.1.10
    restart: unless-stopped
    environment:
      TAP_BIND: ":2480"
      TAP_PLC_URL: https://plc.tngl.boltless.dev
      TAP_RELAY_URL: https://pds.tngl.boltless.dev # PDS can be used as basic relay without collectiondir.
      TAP_DATABASE_URL: postgres://tnglr:tnglr@postgres:5432/mirror_tap?sslmode=disable
      TAP_COLLECTION_FILTERS: sh.tangled.repo
      TAP_SIGNAL_COLLECTION: sh.tangled.repo
      TAP_RESYNC_PARALLELISM: "10"
      TAP_RETRY_TIMEOUT: 60s
    volumes:
      - ./localinfra/certs/root.crt:/etc/ssl/certs/ca-certificates.crt:ro
    depends_on:
      postgres:
        condition: service_started
      pds:
        condition: service_healthy
    networks: [tngl]

  knotmirror:
    build:
      context: .
      dockerfile: localinfra/knotmirror.Dockerfile
    restart: unless-stopped
    environment:
      MIRROR_LISTEN: 0.0.0.0:7000
      MIRROR_ADMIN_LISTEN: 0.0.0.0:7200
      MIRROR_HOSTNAME: mirror.tngl.boltless.dev
      MIRROR_TAP_URL: http://knotmirror-tap:2480
      MIRROR_DB_URL: postgres://tnglr:tnglr@postgres:5432/mirror?sslmode=disable
      MIRROR_REDIS_ADDR: redis:6379
      MIRROR_PLC_URL: https://plc.tngl.boltless.dev
      MIRROR_GIT_BASEPATH: /data/repos
      MIRROR_KNOT_USE_SSL: "true"
      MIRROR_KNOT_SSRF: "false"
      MIRROR_RESYNC_PARALLELISM: "4"
      MIRROR_APPVIEW_URL: http://appview:3000
      MIRROR_SEARCH_ZOEKT_URL: https://zoekt.tngl.boltless.dev/indexserver
    volumes:
      - knotmirror-data:/data
      - ./localinfra/certs/root.crt:/usr/local/share/ca-certificates/caddy.crt:ro
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:7000/"]
      interval: 2s
      timeout: 2s
      retries: 30
      start_period: 5s
    ports:
      - "7201:7200"
    depends_on:
      postgres:
        condition: service_started
      knotmirror-tap:
        condition: service_started
    networks: [tngl]

  zoekt-webserver:
    build:
      context: https://tangled.org/boltless.me/zoekt.git#tngl
    volumes:
      - zoekt-index:/data/index
      - ./localinfra/certs/root.crt:/etc/ssl/certs/caddy.crt:ro
    command: ["zoekt-webserver", "-index", "/data/index", "-pprof", "-rpc"]
    networks: [tngl]

  zoekt-tngl-indexserver:
    build:
      context: .
      dockerfile: localinfra/zoekt-tngl-indexserver.Dockerfile
    environment:
      TANGLED_ZOEKT_INDEX_DIR: /data/index
      TANGLED_ZOEKT_PLC_URL: https://plc.tngl.boltless.dev
      TANGLED_ZOEKT_APPVIEW_URL: http://127.0.0.1:3000
      TANGLED_ZOEKT_ALLOW_HTTP: "true"
    volumes:
      - zoekt-index:/data/index
      - ./localinfra/certs/root.crt:/etc/ssl/certs/caddy.crt:ro
    command: ["zoekt-tngl-indexserver", "serve"]
    networks: [tngl]

  tailwind:
    build:
      context: .
      dockerfile: localinfra/tailwind.Dockerfile
    restart: unless-stopped
    working_dir: /build
    init: true
    volumes:
      - ./tailwind.config.js:/build/tailwind.config.js:ro
      - ./input.css:/build/input.css:ro
      - ./appview/pages/templates:/build/appview/pages/templates:ro
      - ./docs:/build/docs:ro
      - ./blog/templates:/build/blog/templates:ro
      - ./blog/posts:/build/blog/posts:ro
      - ./appview/pages/static:/build/appview/pages/static
    command:
      ["-i", "input.css", "-o", "appview/pages/static/tw.css", "--watch=always"]
    network_mode: none

  appview:
    build:
      context: .
      dockerfile: localinfra/appview.Dockerfile
    restart: unless-stopped
    environment:
      TANGLED_DEV: "true"
      TANGLED_APPVIEW_HOST: 127.0.0.1:3000
      TANGLED_DB_PATH: /var/lib/appview/appview.db
      TANGLED_PLC_URL: https://plc.tngl.boltless.dev
      TANGLED_JETSTREAM_ENDPOINT: wss://jetstream.tngl.boltless.dev/subscribe
      TANGLED_REDIS_ADDR: redis:6379
      TANGLED_KNOTMIRROR_URL: https://mirror.tngl.boltless.dev
      TANGLED_CODESEARCH_ZOEKT_URL: https://zoekt.tngl.boltless.dev
      TANGLED_SSH_ENABLED: "true"
      TANGLED_SSH_LISTEN_ADDR: "0.0.0.0:3333"
    ports:
      - "3000:3000"
      - "3333:3333"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/"]
      interval: 5s
      timeout: 2s
      retries: 15
      start_period: 10s
    volumes:
      - .:/src:cached
      - go-cache:/go/cache
      - go-mod-cache:/go/mod
      - appview-data:/var/lib/appview
      - init-state:/shared:ro
      - ./localinfra/certs/root.crt:/usr/local/share/ca-certificates/caddy.crt:ro
    depends_on:
      redis:
        condition: service_started
      pds:
        condition: service_healthy
      init-accounts:
        condition: service_completed_successfully
    networks: [tngl]

  pdsls:
    build:
      context: .
      dockerfile: localinfra/pdsls.Dockerfile
    restart: unless-stopped
    ports:
      - "8081:80"
    depends_on:
      init-accounts:
        condition: service_completed_successfully
    networks: [tngl]

  hydrant:
    build:
      context: https://tangled.org/ptr.pet/hydrant.git#main
    restart: unless-stopped
    environment:
      HYDRANT_API_BIND: 0.0.0.0:3000
      HYDRANT_DATABASE_PATH: /data/hydrant.db
      HYDRANT_RELAY_HOSTS: pds::wss://pds.tngl.boltless.dev
      HYDRANT_CRAWLER_URLS: list_repos::https://pds.tngl.boltless.dev
      HYDRANT_PLC_URL: https://plc.tngl.boltless.dev
      HYDRANT_FILTER_COLLECTIONS: sh.tangled.*
      HYDRANT_FILTER_SIGNALS: sh.tangled.actor.profile,sh.tangled.feed.comment,sh.tangled.feed.reaction,sh.tangled.feed.star,sh.tangled.git.refUpdate,sh.tangled.graph.follow,sh.tangled.graph.vouch,sh.tangled.knot,sh.tangled.knot.member,sh.tangled.label.definition,sh.tangled.label.op,sh.tangled.pipeline,sh.tangled.pipeline.status,sh.tangled.publicKey,sh.tangled.repo,sh.tangled.repo.artifact,sh.tangled.repo.collaborator,sh.tangled.repo.issue,sh.tangled.repo.issue.comment,sh.tangled.repo.issue.state,sh.tangled.repo.pull,sh.tangled.repo.pull.comment,sh.tangled.repo.pull.status,sh.tangled.spindle,sh.tangled.spindle.member,sh.tangled.string
      HYDRANT_BACKFILL_STRATEGY: sparse-filter
      HYDRANT_VERIFY_SIGNATURES: none
      RUST_LOG: info
    volumes:
      - hydrant-data:/data
      - ./localinfra/certs/root.crt:/etc/ssl/certs/ca-certificates.crt:ro
    healthcheck:
      test: ["CMD", "bash", "-c", "echo > /dev/tcp/127.0.0.1/3000"]
      interval: 5s
      timeout: 2s
      retries: 15
      start_period: 10s
    depends_on:
      plc:
        condition: service_started
      pds:
        condition: service_healthy
      caddy:
        condition: service_started
      init-accounts:
        condition: service_completed_successfully
    networks: [tngl]
  bobbin:
    build:
      context: .
      dockerfile: localinfra/bobbin.Dockerfile
    restart: unless-stopped
    environment:
      BOBBIN_BIND: 0.0.0.0:8090
      BOBBIN_HYDRANT_URL: http://hydrant:3000
      BOBBIN_SLINGSHOT_URL: http://hydrant:3000
      BOBBIN_KNOT_ALLOW_PRIVATE: "true"
      BOBBIN_KNOT_REQUIRE_HTTPS: "false"
      BOBBIN_LOG: info
    volumes:
      - .:/src:cached
      - bobbin-cargo:/cargo
      - bobbin-target:/target
      - ./localinfra/certs/root.crt:/etc/ssl/certs/ca-certificates.crt:ro
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8090/xrpc/sh.tangled.bobbin.getCoverage"]
      interval: 5s
      timeout: 2s
      retries: 15
      start_period: 10s
    depends_on:
      hydrant:
        condition: service_started
      caddy:
        condition: service_started
    networks: [tngl]

  web:
    build:
      context: .
      dockerfile: localinfra/web.Dockerfile
    restart: unless-stopped
    environment:
      BOBBIN_URL: https://bobbin.tngl.boltless.dev
      KNOTMIRROR_URL: https://mirror.tngl.boltless.dev
      CAMO_URL: https://camo.tngl.boltless.dev
      CAMO_SHARED_SECRET: localinfra-camo-secret
      AVATAR_URL: https://avatar.tngl.boltless.dev
      AVATAR_SHARED_SECRET: localinfra-avatar-secret
      VITE_HANDLE_RESOLVER_URL: https://pds.tngl.boltless.dev
      VITE_PLC_DIRECTORY_URL: https://plc.tngl.boltless.dev
      # host-side port; keeps the oauth loopback redirect consistent
      VITE_DEV_PORT: "5174"
      # the bind mount below does not deliver inotify, see vite.config.ts
      VITE_WATCH_POLL: "true"
      NODE_EXTRA_CA_CERTS: /caddy-ca/root.crt
    ports:
      - "5174:5174"
    volumes:
      - ./web:/src:cached
      # shadow generated dirs so the host tree stays untouched
      - web-node-modules:/src/node_modules
      - web-svelte-kit:/src/.svelte-kit
      - ./localinfra/certs/root.crt:/caddy-ca/root.crt:ro
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:5174/').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
      interval: 5s
      timeout: 2s
      retries: 30
      start_period: 20s
    depends_on:
      bobbin:
        condition: service_started
      caddy:
        condition: service_started
    networks: [tngl]

  camo:
    build:
      context: .
      dockerfile: localinfra/camo.Dockerfile
    restart: unless-stopped
    environment:
      CAMO_SHARED_SECRET: localinfra-camo-secret
      WRANGLER_SEND_METRICS: "false"
    volumes:
      - ./camo:/src:cached
      # shadow generated dirs so the host tree stays untouched
      - camo-node-modules:/src/node_modules
      - camo-wrangler:/src/.wrangler
      - ./localinfra/certs/root.crt:/caddy-ca/root.crt:ro
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
      interval: 5s
      timeout: 2s
      retries: 30
      start_period: 30s
    networks: [tngl]

  avatar:
    build:
      context: .
      dockerfile: localinfra/avatar.Dockerfile
    restart: unless-stopped
    environment:
      AVATAR_SHARED_SECRET: localinfra-avatar-secret
      PLC_DIRECTORY_URL: https://plc.tngl.boltless.dev
      WRANGLER_SEND_METRICS: "false"
    volumes:
      - ./avatar:/src:cached
      - avatar-node-modules:/src/node_modules
      - avatar-wrangler:/src/.wrangler
      - ./localinfra/certs/root.crt:/caddy-ca/root.crt:ro
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
      interval: 5s
      timeout: 2s
      retries: 30
      start_period: 30s
    networks: [tngl]

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "8090:8090"
    volumes:
      - ./localinfra/Caddyfile:/etc/caddy/Caddyfile
      - ./localinfra/certs:/etc/caddy/certs:ro
      - caddy-data:/data
    networks:
      tngl:
        aliases:
          - plc.tngl.boltless.dev
          - pds.tngl.boltless.dev
          - alice.pds.tngl.boltless.dev
          - bob.pds.tngl.boltless.dev
          - charlie.pds.tngl.boltless.dev
          - david.pds.tngl.boltless.dev
          - jetstream.tngl.boltless.dev
          - knot.tngl.boltless.dev
          - spindle.tngl.boltless.dev
          - tngl.boltless.dev
          - mirror.tngl.boltless.dev
          - zoekt.tngl.boltless.dev
          - pdsls.tngl.boltless.dev
          - bobbin.tngl.boltless.dev
          - camo.tngl.boltless.dev
          - avatar.tngl.boltless.dev

volumes:
  caddy-data:
  postgres-data:
  pds-data:
  jetstream-data:
  knot-data:
  knot-ssh-keys:
  knotmirror-data:
  zoekt-index:
  spindle-data:
  spindle-logs:
  ncps-data:
  init-state:
  go-cache:
  go-mod-cache:
  appview-data:
  hydrant-data:
  bobbin-cargo:
  bobbin-target:
  web-node-modules:
  web-svelte-kit:
  camo-node-modules:
  camo-wrangler:
  avatar-node-modules:
  avatar-wrangler:

networks:
  tngl:
    driver: bridge
    # Public-looking subnet so SSRF checks see container IPs as "public".
    # RFC1918 + doc/benchmark ranges are blocklisted; 11.x is unrouted on
    # the public internet, so it passes the check and won't collide.
    ipam:
      config:
        - subnet: 11.0.0.0/24
