Set SO_REUSEADDR on the web admin's TCPServer — the actual root cause

Live-confirmed the retry fix from the previous commit wasn't enough:
still "Address already in use" after 20s of retries (10 attempts,
2s backoff), only to succeed on its own sometime after that. That
delay pattern is TIME_WAIT, not a process-death race — and this code
was never going to avoid it, because socketserver.TCPServer defaults
allow_reuse_address to False. (http.server.HTTPServer sets this for
you; the plain base class used here does not.) Without SO_REUSEADDR,
the kernel can refuse to rebind a port with a lingering TIME_WAIT
socket from the previous instance for up to 60s, regardless of
whether that old process is even still alive — which is also why the
entrypoint.sh fix waiting for the process to exit didn't help either.

Set socketserver.TCPServer.allow_reuse_address = True before binding.
This is the standard fix for exactly this symptom. Keeping the retry
loop from the previous commit too, for the (now much smaller) window
where network_mode: host still has no Docker-managed port mapping to
instantly free.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi
This commit is contained in:
Claude
2026-07-20 19:18:09 +00:00
parent 4d2829f116
commit 104fd26b6d
+16 -6
View File
@@ -6135,12 +6135,22 @@ class WebAdminHandler(http.server.BaseHTTPRequestHandler):
self.wfile.write(json.dumps(data).encode())
def main():
# A container recreate under network_mode: host depends on the *previous*
# container's web admin process actually dying before the port is free —
# there's no Docker-managed port mapping to instantly release it like
# there would be in bridge mode. A fast recreate-right-after-recreate can
# briefly race that teardown. Retry instead of crashing on the first
# failure, which otherwise means the web admin silently never comes up.
# socketserver.TCPServer (unlike http.server.HTTPServer, which sets this
# itself) defaults allow_reuse_address to False — without SO_REUSEADDR,
# the kernel can refuse to rebind this port for up to 60s while any
# TIME_WAIT socket from the previous instance lingers, regardless of
# whether that old process is even still alive. Live-confirmed: still
# failing after 20s of retries, only to succeed on its own well after
# that. This was the actual bug — SO_REUSEADDR is the standard fix for
# exactly this "restarted a TCP server, port still busy" symptom.
socketserver.TCPServer.allow_reuse_address = True
# Still retry on top of that: under network_mode: host there's no
# Docker-managed port mapping to instantly release like there'd be in
# bridge mode, so a fast recreate-right-after-recreate can still
# briefly race the previous process's own shutdown. Retry instead of
# crashing on the first failure, which otherwise means the web admin
# silently never comes up.
httpd = None
last_err = None
for attempt in range(10):