Highlights
A patch release that closes the docker hijack regression class that crept in alongside v0.25.0's TLS/scheme rework, plus a webhook multi-instance dedup bug and the long-standing url-only-webhook documentation gap. Anyone running run_exec jobs through tecnativa/docker-socket-proxy or any plain-TCP/HTTP Docker host should upgrade. Anyone wiring more than one webhook per job (typically one for success, one for error) needs the multi-webhook fix. Anyone who tried to configure a webhook with only a url = ... since v0.16.0 will find that it actually works now.
🐳 Docker daemon connectivity
DOCKER_HOST=tcp://...(plain HTTP)run_execjobs work again. v0.25.0 regressed plain-tcp://hijack APIs (ContainerExecAttach,ContainerAttach,ContainerLogs --follow) with the unhelpful errortls: first record does not look like a TLS handshake. Container discovery kept working because the regular HTTP path was unaffected; onlyrun_execand friends failed. Root cause was a Go stdlib × Docker SDK interaction:net/httplazily auto-configures HTTP/2 on the first request and allocatesTLSClientConfigin place for ALPN — and the SDK's hijack dialer reads that as "TLS required" and dialstls.Dialagainst your plaintext daemon. The fix suppresses the lazy auto-config on non-TLS transports. TLS Docker hosts (https://,tcp+tls://) are unaffected — ALPN h2 negotiation still works there. (#681, fixes #668)DOCKER_HOST=http://...hijack now works for the same APIs. Same SDK dialer, different failure mode:net.Dial("http", addr)is rejected by Go'snetpackage ("http"isn't a valid network name) so the call surfaced asdial http: unknown network http. The fix installs an explicit TCPDialContextso the SDK picks our dialer instead of reaching the broken fallback. Path-bearing hosts (http://daemon:port/v1.43) and IPv6 literals (http://[::1]:2375) round-trip cleanly viaurl.Parse, matching how the SDK already handlestcp://. (#686, fixes #682)
🔁 Webhooks
- Url-only webhooks finally work out of the box. The
docs/webhooks.md"Custom Webhooks" section has been promising since v0.16.0 that you could declare a webhook with justurl = ...and have Ofelia POST a JSON payload to it — but the code path actually returnedpreset specification cannot be emptyand refused to attach. v0.25.1 ships a new bundledjson-postpreset and a[global] webhook-default-presetselector that ships withjson-postas the default fallback. A webhook with onlyurl = ...(or Docker labelofelia.webhook.<name>.url: ...) now attaches and fires, no custom preset YAML required.- Audit-and-pin upgrade impact: a stale URL-only config that previously sat inert will now actually POST to whatever's in
url. Pinwebhook-allowed-hoststo a specific allow-list if you want to restrict egress; setwebhook-default-preset =(empty) to keep pre-upgrade behavior and require every webhook to declarepresetexplicitly. Three-state semantics: nil = "operator did not set" (use bundled fallback), non-nil empty = "explicit opt-out", non-nil non-empty = "operator's chosen fallback name". (#677, fixes #676)
- Audit-and-pin upgrade impact: a stale URL-only config that previously sat inert will now actually POST to whatever's in
- Jobs that reference more than one webhook now fire every listed webhook. The middleware container deduplicated middlewares by Go type, so handing it two
*Webhookinstances (e.g. one withtrigger: success, one withtrigger: error) kept only the first and silently dropped the rest — the error webhook attached to nothing and never ran. The same dedup also shadowed scheduler-level webhooks against per-job webhooks during propagation, so any job declaringwebhooks:silently lost global notifications too. The fix attaches a single per-job composite carrying the union of[global] webhook-webhooksand the job's ownwebhooksselector, deduplicated by name. The previously-silent failure path (unknown webhook name, preset-load failure, missing required variable) now emits anslog.Errorkeyed by job name and webhook list so misconfigurations show up in the log. (#671, fixes #670) - Webhook retries no longer hold the daemon hostage at shutdown.
(*Webhook).sendWithRetry's inter-attempt backoff used a baretime.Sleepthat ignored ctx cancellation, soSIGTERMmid-retry pinned a goroutine for up toretry-delay × retry-count. The in-flight HTTP request had the same shape — its context was derived fromcontext.Background(), so even a hung POST blocked shutdown for the fulltimeout. Both halves now drain on scheduler cancellation. Worst-case shutdown contribution from a single webhook drops fromretry-delay × retry-count + timeout(multi-minute on aggressive retry budgets) to roughly one network round-trip on cancel. Callers see an error chain wrappingcontext.Canceled/context.DeadlineExceededrather than the previous "all N attempts failed" message after the full timeout elapsed. (#685, fixes #673)
📖 Docs
- New
docs/TROUBLESHOOTING.mdsections for both hijack failure modes (tls: first record does not look like a TLS handshakeontcp://,dial http: unknown network httponhttp://), slotted alongside the existing #605 socket-proxy section so operators scanning by symptom land on the right entry. docs/webhooks.mdretry-config table now documents the shutdown drain semantics —retry-count × retry-delay + timeoutbounds the worst-case daemon-shutdown contribution from any single webhook, and the retry loop observes scheduler cancellation since this release.- Webhook lifecycle flowchart added to
docs/webhooks.mdfor the trigger / dedup / retry / shutdown story. (#680)
Dependencies
No direct dependency bumps in this release. The Go toolchain and module graph stay at v0.25.0's pinned versions.
Thanks
Special thanks to the external reporters whose issues drove the bugfixes in this release:
- @groknt for #668 — reporting that
run_execjobs failed with the cryptic TLS handshake error on plain-tcp://socket-proxy setups, and following up with the exact diagnostic dump (env, debug logs, container labels, full compose file) that let us reproduce the bug in a self-contained Go program and isolate it to the Go std-lib × Docker SDK interaction. The reproducer also surfaced the siblinghttp://failure mode that ships in this release as #682 / #686. - @techsolo12 for #670 — reporting that error-trigger webhooks were silently being dropped from jobs that also declared a success-trigger webhook. The repro pointed straight at the type-keyed dedup in
middlewareContainer.Use, which had been a latent footgun waiting for the first N-instance middleware to land.
Container image
ghcr.io/netresearch/ofelia:0.25.1
ghcr.io/netresearch/ofelia:0.25
ghcr.io/netresearch/ofelia:0
ghcr.io/netresearch/ofelia:latest
Verification
All release assets (binaries, SBOMs, checksums) are cosign-signed with keyless OIDC and include SLSA Level 3 build provenance. Verify with:
cosign verify-blob \
--bundle ofelia-linux-amd64.bundle \
--certificate-identity-regexp 'https://github\.com/netresearch/(\.github|ofelia)' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ofelia-linux-amd64
Full Changelog: v0.25.0...v0.25.1