services:
  scheduler:
    image: irdb-scheduler:latest
    build: { context: ./scheduler }
    environment:
      INTERNAL_JOB_TOKEN: ${INTERNAL_JOB_TOKEN}
    # SEC_REVIEW F25: share the api container's network namespace so the
    # baked crontab can hit /internal/jobs/tick over loopback. The api's
    # /internal/* gate is restricted to 127.0.0.1/::1 (Caddy + PHP), so
    # the previous `api:8081` URL from a sibling docker-bridge peer
    # would now 404 — and that's the goal: a malicious neighbour on the
    # same bridge can no longer reach /internal/*. Side effect: this
    # service has no DNS / its own ports, but it needs neither.
    network_mode: "service:api"
    # SEC_REVIEW F22: dependencies (curl, tini, ca-certificates) are now
    # baked into the image at build time with pinned versions, against a
    # digest-pinned alpine base. The previous `image: alpine:3` +
    # `apk add` at container start trusted the apk mirror on every
    # restart and would have given a mirror compromise a foothold in the
    # container that holds INTERNAL_JOB_TOKEN.
    read_only: true
    # busybox crond writes a tiny tempfile when the schedule fires; /run
    # has to be writable for that. Everything else stays read-only.
    tmpfs:
      - /run:mode=0755
      - /tmp:mode=1777
    # busybox crond calls initgroups() before each exec, which needs
    # CAP_SETGID even when the target user is the same root it is
    # already running as — full cap_drop crashes it with
    # "can't set groups: Operation not permitted". Hardening the
    # process to non-root would mean shipping a custom cron binary;
    # not worth the maintenance cost given the container has no
    # persistent volume, no exposed port, and only INTERNAL_JOB_TOKEN
    # in env. `no-new-privileges` is still useful: there is no setuid
    # binary in the image and we want to keep it that way.
    security_opt:
      - no-new-privileges:true
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
