Publishers no longer need external hosting at all. The service runs its
own s3d (SiaFoundation gateway) as a Docker sidecar on the internal
compose network; scan-clean uploads land there via PUT; readers fetch
through /dl/<sha256> on prometheus.x which proxies back out from s3d.
- deploy/docker-compose.yml: s3d service added. Image is
ghcr.io/siafoundation/s3d:latest, data folder bind-mounted at
./s3d-data/ so the operator can seed it from a Pithos connection
export. Only prometheus-internal network — never public.
- deploy/setup-s3d.sh: one-time import of the Pithos connection
tarball, writes a fresh s3d.yml with a random adminPassword, mints
an S3 access key for the Prometheus service, appends
PROMETHEUS_S3_* lines to .env. Idempotent on re-run.
- service/storage.js: endpoint now defaults to the internal s3d
(http://s3d:8000); publicBase defaults to https://prometheus.x so
download URLs flow through the service's own /dl handler. Bucket is
auto-created on first use so a fresh data folder bootstraps.
Added getObjectStream() for the read side.
- service/downloads.js: GET/HEAD /dl/<sha256> — forwards Range,
content-type and content-length from s3d, drops s3d-specific
headers (x-amz-*), sets immutable long-cache. Hash is validated
against ^[0-9a-f]{64}$ before touching upstream.
- service/server.js: route table wires /dl/* to the handler above.
Publishers no longer need to self-host. When the service is configured
with PROMETHEUS_S3_* credentials pointing at the hosted s3d
(s3.silentmode.st), every clean upload is pushed to
s3://${bucket}/${prefix}<sha256>.bin and the public URL comes back on
/api/upload as `hosted_url`. Step 2 of the submit form pre-fills the
Download URL input with that URL — publishers can overwrite if they
prefer their own hosting; by default they ship nothing.
- service/s3.js: SigV4 S3 client vendored from Pithos/core/s3.js so bug
fixes port by file-copy. Path-style, UNSIGNED-PAYLOAD streaming PUT,
Content-Length required (s3d chunked encoding does not round-trip
cleanly for large bodies).
- service/storage.js: thin wrapper exposing isConfigured() and
uploadScannedFile(). Idempotent on sha256 — same bytes → same key.
Failure is non-fatal; the publisher can still paste their own URL.
- service/uploads.js: after rename-to-final, push to Sia if configured.
Added `hosted_url` to the response (null when storage off).
- service/pages.js: step 2 pre-fills download_url when hosted_url is
present; hint text explains the override path.
- docker-compose.yml: PROMETHEUS_S3_* env plumbed through, with sane
defaults for bucket/prefix (bns + prometheus-storage/).
ClamAV ships with StreamMaxLength=25 MB, MaxFileSize=100 MB, MaxScanSize=
400 MB — mobile APKs over 25 MB came back as "INSTREAM size limit
exceeded" from the scanner, which the service wrapped in a 502 JSON
body, which Caddy sometimes replaced with its own HTML error page under
flaky mobile transport. The browser then saw 'Unexpected token <' and
told the user 'Network error'.
Overlay a minimal clamd.conf (mirrors the image default plus the three
raised limits, all pinned at 2 GiB = 2147483648 bytes) via a read-only
bind mount. Rebuild not needed — only clamav restarts.
Before this: service's /healthz always 503'd and the upload endpoint
rejected every submission with scan verdict=error, because clamd was not
installed anywhere reachable. Service worked for the seeded browse view
but no publisher could POST a listing.
Now: a `clamav` sibling container runs the official clamav/clamav:stable
image on an internal docker network only Prometheus can reach. The
Prometheus service connects via CLAMD_HOST=clamav. Signatures live in a
dedicated volume so restarts reuse the ~250 MB freshclam downloaded on
first start.
Two networks on the Prometheus container now:
prometheus-internal bridge, service + clamav only; clamd never exposed
hephaestus external hephaestus_default, so Hephaestus's Caddy
continues to route prometheus.x → prometheus:3700
depends_on with condition=service_healthy means Prometheus waits for
clamd's healthcheck to pass before starting. clamav's start_period is
5 min to accommodate the signature download; the healthcheck itself
is clamdcheck.sh / clamdscan --ping with a 10s timeout per probe.
Deploy on the VPS:
cd ~/prometheus && git pull && cd deploy
docker compose pull clamav # first time, 500+ MB
docker compose up -d # Prometheus will wait for clamav healthy
The VPS runs Caddy inside Docker as part of the Hephaestus stack (not
system-wide angie as the earlier deploy/README assumed), so the shape
of the deploy is: Prometheus runs as its own small docker-compose that
attaches to Hephaestus's existing network, and Hephaestus's Caddy
reverse-proxies prometheus.x → prometheus:3700.
Dockerfile: node:22-alpine base (keeps the image ~55 MB and gives us
node:sqlite stable), tini for signal handling, non-root `node` user,
one /data volume holding the DB + ed25519 service key + uploads +
snapshot build output.
docker-compose: one service, attaches hephaestus_default as external
network, env sourced from an .env file alongside the compose. The .env
is NOT committed (contains ADMIN_PUBLISHER, optional VirusTotal key,
optional payment config). Healthcheck polls /healthz.
The deploy/prometheus-service.service systemd unit from the earlier
commit is now unused on this host (kept for anyone deploying natively
on a non-dockered box).