An OAuth Callback Refused in a Host-Mode Container
My first guess was a ports block in compose. Host-mode already shares the host loopback, and ports actually breaks compose validation.
Connection refused on the callback, even though the consent page already showed
I opened the browser on the host, the Spotify consent page was right there, I clicked agree. What I got instead was connection refused when the callback hit http://[::1]:43827/spotify/callback. I had just migrated Hermes into a Docker container with network_mode: host, in the Projects directory on the host. My first guess was simple: the container doesn't expose ports.
So I opened docker-compose.yml and added a ports block:
services:
hermes:
network_mode: host
ports:
- "43827:43827"
Then I ran docker compose up. It exploded immediately with: conflicting options: port publishing and the container type network mode.
My guess was completely wrong. Host-mode can't publish ports — not a missing config, it's simply not allowed. If you've ever wondered why ports clash between containers, the root cause is often the same: two host-mode containers fighting over one port in a namespace they share.
Loopback belongs to the host, and host-mode shares it anyway
Loopback is per network namespace. When a container runs in host-mode, it shares the host's network namespace in full. A callback server inside the container binds straight to the host stack, loopback interface included. The browser on the host hits the same [::1] on an ephemeral port and the connection just works — no ports: needed. It's the ports: line that breaks compose validation.
My take: ports: under host-mode shouldn't just be ignored — erroring out is the right call, and it's a feature, not a bug. It forces you to understand how network namespaces work instead of mashing the expose button. Docker documents this themselves: host-mode containers get no IP of their own and port mappings are discarded — see the host network driver docs.
Once that clicked, every loopback OAuth CLI just worked. The standard explains why. RFC 8252 section 7.3 says the authorization server MUST accept any port for loopback redirect URIs, because the ephemeral port is picked at request time RFC 8252. Google documents the loopback IP flow with addresses like [::1] as the official pattern for desktop apps, with a MITM warning for other apps on the same loopback Google loopback migration guide. Real implementers do this too: Duende IdentityServer ships AddAppAuthRedirectUriValidator, which accepts any port when the client registers http://[::1] and PKCE is required Duende's port-agnostic validator.
If you're building an agent or CLI with the same pattern, don't smash ports: into compose. Host-mode plus a loopback bind is enough. One thing stays mandatory: PKCE, because a loopback callback can be intercepted by another app on the same machine. That's not a Docker problem — it's a property of loopback itself.