Ingress
One door in.
Exactly one container is published to the host. nginx-rtmp terminates TLS, accepts the RTMP push from OBS on 1935, and serves the HLS segments and static player over 443.
Currently live
A live streaming platform with its own player, chat, accounts, and payments — served from a server in my house rather than a platform that owns the audience.
Ingress
Exactly one container is published to the host. nginx-rtmp terminates TLS, accepts the RTMP push from OBS on 1935, and serves the HLS segments and static player over 443.
Services
Auth, chat, and payments sit on a private bridge network with no port mapping at all. Nothing outside the host can address them directly — every request arrives proxied, or it does not arrive.
State
Postgres 16 is the only stateful service. Auth and payments declare a health-gated dependency, so neither starts issuing queries until the database reports ready — no boot-order race to lose.
Internal calls
When a tip clears, payments calls the chat service directly across the internal network to push the event into the live WebSocket room. That traffic never touches the public interface.
Renewal
A certbot loop wakes every twelve hours. On a successful renewal its deploy hook opens the Docker socket and sends nginx a HUP — a reload, not a restart, so no viewer drops mid-stream.
Data model
Everything a person owns points back to users. Deletion is chosen per
relation: sessions, tokens and bans cascade, because they mean nothing without the
account; chat messages, tips and schedule events set null, because the history should
outlive it.
The physical layer
No cloud encoder, no managed origin. The signal leaves a capture card, gets encoded on a dedicated box, and is served from a workstation I own and administer.
Source
Capture & encode
Origin
A problem worth solving
Certificates expire every ninety days. Renewing them is the easy part — the hard part is telling the running proxy to pick up the new files without dropping anyone mid-stream.
A restart would work and would also kill every open connection. The
correct signal is SIGHUP, which makes nginx reload its
configuration and certificates in place. But the certbot container has
no Docker CLI to send it with, and installing one to run a single
command each quarter is the kind of dependency that rots.
So the deploy hook speaks to the Docker daemon directly: a raw HTTP request over the mounted Unix socket, written against the standard library and nothing else. Thirty lines, no package manager, no drift.
request = (
"POST /containers/nginx-rtmp/kill?signal=HUP HTTP/1.1\r\n"
"Host: localhost\r\n"
"Content-Length: 0\r\n"
"\r\n"
)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect("/var/run/docker.sock")
sock.sendall(request.encode("ascii"))
response = sock.recv(1024).decode("ascii", errors="replace")
sock.close()
return 0 if " 204 " in response else 1 Hardening
A permissive CORS default. A WebSocket handshake that accepted every origin. Database calls with no timeout, able to exhaust the connection pool under load. Each one found in my own code, documented with its file and line, and fixed.
Shipping software is table stakes. Going back through it adversarially, writing down the failure mode in plain language, and closing it out is the part that actually keeps a service up.