Follow-up to Directus CMS stack adoption (2026-05-22). Original REPORT.md §05 picked Cloudflare Images (managed) for image transforms and Bunny Stream (managed) for video. Operator now wants OSS-self-host alternatives surveyed so the stack can keep media optimization in-VPS rather than outsourced. Audio + documents/PDF were gaps in the original stack — included here for completeness.
Posture: OSS-self-host, R2 origin, Caddy reverse-proxy, Bunny CDN edge, per-client Docker on shared VPS. On-the-fly transform proxy preferred.
Sibling files (verified candidate matrices, full per-axis scoring):
- images.md · video.md · audio.md · docs-pdf.md
| Axis | Pick | License | Stars | Why |
|---|---|---|---|---|
| Image | imgproxy (DarthSim/imgproxy) | Apache-2.0 | 10.8k | Native R2 origin, HMAC signed URLs, AVIF+WebP+JXL, stateless Go+libvips, designed for reverse-proxy + CDN topology. Score 10/10. |
| Video | shaka-packager (shaka-project) + Eyevinn shaka-packager-s3 wrapper + Eyevinn encore orchestrator |
BSD-3 / MIT | 3.7k / 2 / 80 | Google-maintained packager; S3-in/out wrapper for R2; ffmpeg ABR-mezzanine orchestrator. CMAF + HLS + DASH + Widevine/FairPlay/PlayReady DRM. Score 10/10. Caveat: S3 wrapper is single-maintainer 2★ — fork into Hermes-controlled vendored copy. |
| Audio | FFmpeg + bbc/audiowaveform in a ~300-LOC stateless Hermes-owned shim | (FFmpeg LGPL+) / GPL-3 | — / 2.1k | No off-the-shelf candidate fits — build the shim. FFmpeg loudnorm covers EBU R128; audiowaveform covers waveform PNG; output Opus + AAC + MP3. Liquidsoap (1.6k★, active) reserved as fallback if scheduled podcast assembly enters scope. |
| Docs/PDF | Gotenberg (gotenberg/gotenberg) + mupdf-tools sidecar | MIT / AGPL-3 | 12.3k / 2.8k | Single Docker container REST API wrapping LibreOffice + Chromium + PDFtk; covers PDF/DOCX/XLSX/PPTX/ODT with native async webhooks. v8.33.0 released 2026-05-29 (active). MuPDF gives fast PDF→PNG thumbnails. Score 10/10. |
Pattern observed: all 4 axes converge on stateless transform container behind Caddy, R2 source + target, content-addressed cache, Bunny CDN fronts cache. This is a real fleet-wide pattern, not coincidence. One Caddy block per axis, four images per client tenant. Stack composable.
# docker-compose.yml (per-client tenant)
services:
directus:
image: directus/directus:latest
environment:
STORAGE_LOCATIONS: cloud
STORAGE_CLOUD_DRIVER: s3
STORAGE_CLOUD_ENDPOINT: ${R2_ENDPOINT}
STORAGE_CLOUD_KEY: ${R2_KEY}
STORAGE_CLOUD_SECRET: ${R2_SECRET}
STORAGE_CLOUD_BUCKET: ${R2_BUCKET}
imgproxy:
image: ghcr.io/imgproxy/imgproxy:latest
environment:
IMGPROXY_S3_ENDPOINT: ${R2_ENDPOINT}
IMGPROXY_USE_S3: "true"
IMGPROXY_KEY: ${IMGPROXY_HMAC_KEY}
IMGPROXY_SALT: ${IMGPROXY_HMAC_SALT}
gotenberg:
image: gotenberg/gotenberg:8
command: ["gotenberg", "--api-port=3000", "--webhook-allow-list=.*"]
shaka-packager-s3:
image: ghcr.io/eyevinn/shaka-packager-s3:latest # to be vendored — see Caveat
environment:
S3_ENDPOINT: ${R2_ENDPOINT}
encore:
image: eyevinnplatform/encore:latest
environment:
ENCORE_S3_ENDPOINT: ${R2_ENDPOINT}
audio-shim:
build: ./services/audio-shim # Hermes-owned ~300 LOC
environment:
S3_ENDPOINT: ${R2_ENDPOINT}
# Caddyfile (per-client tenant subdomain)
img.{client}.boas.dev {
reverse_proxy imgproxy:8080
header Cache-Control "public, max-age=31536000, immutable"
}
vid.{client}.boas.dev {
reverse_proxy shaka-packager-s3:8080
header Cache-Control "public, max-age=2592000"
}
doc.{client}.boas.dev {
reverse_proxy gotenberg:3000
}
aud.{client}.boas.dev {
reverse_proxy audio-shim:8000
}
Bunny CDN pull-zone per subdomain. R2 lifecycle policies handle stale-cache eviction.
| Candidate | Axis | Reason |
|---|---|---|
| flyimg | Image | AGPL-3 on public-facing transform endpoint — operator-decision blocker; otherwise scored 9/10 |
| imaginary | Image | No AVIF; last tagged release 2020 |
| imageflow | Image | imageflow_server removed upstream; .NET-only replacement |
| picfit / lilliput | Image | No AVIF / library not server |
| OvenMediaEngine | Video | AGPL-3 contamination for per-client commercial work |
| PeerTube | Video | AGPL-3 + federated-server scope mismatch |
| Tdarr / Unmanic / FileFlows | Video | Library-optimization farms, not ABR packagers |
| Bento4 | Video | Lacks native S3 (needs rclone); GPL-2 / commercial-license ambiguity. Held as fallback only. |
| loudness-scanner | Audio | Dormant since 2023 |
| AzuraCast | Audio | Webradio appliance — scope mismatch |
| Icecast | Audio | Distribution layer, not processor |
| ONLYOFFICE / Collabora | Docs | Full collaborative editors — too heavy for thumb/preview |
| paperless-ngx | Docs | DMS scope |
| Apache Tika | Docs | Text extraction not preview rendering |
| pdf.js | Docs | Client-side only |
| Stirling-PDF v2 | Docs | License posture NOASSERTION — needs legal review for commercial bundling. v1 is GPL-3, usable. |
Bunny CDN does not disappear. OSS sidecars replace transform, not edge delivery. Bunny pull-zone still fronts each subdomain. Transform sidecars are origin-shielded by CDN cache; absent Bunny, imgproxy/gotenberg eat thundering-herd risk on viral assets.
Storage cost shift. HLS variants typically 2-4× source size at 4-rung ABR ladder. Directus stack budget (originally framed around image-only R2 cost) needs revision if video lands in scope. Per-client cap recommended.
GPU optional, not required. All 4 axes run CPU-only acceptable for VPS posture. NVENC/QSV unlock 4-8× transcode throughput if a client's video catalog grows beyond hundreds of GB — defer until triggered.
On-the-fly transcode is a stretch goal not delivered. Universal pattern in OSS video is pre-transcode-at-upload; no top pick supports per-request transcode. Image, audio, doc axes DO support on-the-fly transform first-class. Accept asymmetry; align Directus upload-webhook to fire encore-job for video, no extra step for the other three.
License posture clean except for 3 flags: flyimg AGPL (image, secondary), Bento4 GPL+commercial (video, fallback only), Stirling-PDF v2 NOASSERTION (docs, fallback only). Top picks across all 4 axes are MIT / Apache-2 / BSD-3 / LGPL — no copyleft contamination on the recommended path.
Audio shim is the only build-vs-adopt deviation. Hermes owns ~300 LOC. Trade-off accepted — no off-the-shelf candidate fits the lightweight on-the-fly transform-proxy shape for audio. Liquidsoap (1.6k★, daily commits) reserved as scope-expansion fallback if scheduled-feed audio enters scope.
| # | Question | Why it matters | Lean |
|---|---|---|---|
| Q1 | Accept Hermes-vendored fork of Eyevinn shaka-packager-s3 wrapper (~500 LOC, BSD-3)? Or upstream-contribute first? |
Single-maintainer 2★ upstream = bus-factor risk on a load-bearing piece | Vendor first, upstream-contribute second |
| Q2 | AGPL posture for client-facing web app sidecars (relevant if flyimg or OvenMediaEngine revisited)? | Reach-of-AGPL across HTTP boundary unsettled in case law | Avoid AGPL on edge sidecars; keep AGPL acceptable for internal-only services |
| Q3 | Smart cropping (saliency-aware) required for image pipeline? | imgproxy has it via Pro tier; thumbor has open ML plugins | Defer — operator add when first client demands |
| Q4 | DRM scope for video — Widevine/FairPlay/PlayReady or token-gated only? | Shaka supports full DRM but cert provisioning is per-client legal work | Token-gated v1; DRM v2 |
| Q5 | Build audio shim now, or defer until first audio client? | Avoid building until validated | Defer — flag in client-onboarding checklist |
| Q6 | Stirling-PDF v1 acceptable as fallback (GPL-3), or hard-reject all license-ambiguous candidates? | v2 is in NOASSERTION; v1 is GPL-3 and safe | Accept v1 as fallback only |
This is a research arc, not an adoption arc (per 2026-05-22 lesson — Pantheon ceremony only when operator explicitly asks for adoption-execution layer). Operator owns:
Hermes will not file these handoffs unsolicited. Watch-list only.
gh api repos/{owner}/{name} calls within sessiongrep -rilE "imgproxy|thumbor|imageflow|libvips" research/ returned 1 hit (nda-and-contracts-self-host-2026-05-06/evidence/docuseal-agency-setup.md — unrelated context); no prior media-optimization arc on file