A healthy container isn't a working feature — read the actual startup logs
John C. Thomas
Founder, BlueWave Projects
We finally had what we needed to light up a real-time feature that had been sitting half-built for weeks — a live vessel-tracking feed for a couple of our sample demos, gated on nothing but an API key. The key arrived. We wired it into the shared backend, rebuilt the image, brought the container up.
Docker said healthy. The build had succeeded. Every signal you'd normally trust said it worked.
The feature did nothing.
Green lights, all the way down
Here's what "checking the deploy" looked like, and why every step of it would have said ship it:
docker compose up -d reported the container recreated and started.docker ps showed it running, health check passing.Four true facts, and the fifth one — the actual feature — was completely inert. None of the first four would ever have told us that, because none of them ask the one question that matters: did the thing we just shipped actually do its job?
The bug was two bugs, and the second one hid the first
The first problem was ordinary: the module's own deploy notes, written months earlier, described wiring it in with FastAPI's older @app.on_event("startup") decorator. The app had since moved to the modern lifespan context-manager pattern. Mixing the two isn't something you want to gamble on in a shared container running a few dozen other routes, so the fix was to call the same startup function from inside the app's actual lifespan, not bolt on a second, older mechanism the rest of the codebase had already left behind. Deploy instructions written into a comment rot exactly like code does — the file was telling us how to wire it into an app that no longer existed.
The second problem is the one worth remembering. The module needed one more Python package for its websocket connection. It wasn't installed. And the code handled that missing dependency about as responsibly as code can:
try:
import websockets
except ImportError:
websockets = NoneLater, at startup: if websockets is None, print a line saying so, and skip starting the feed. No crash. No stack trace. No non-zero exit code. Just one line of text, printed once, sitting between the database-ready message and the templates-loaded message in a startup log forty lines long.
That is good defensive code. It's also a silent failure with better manners.
"Degraded gracefully" is not the same as "worked"
The instinct to wrap a risky import in a try/except is correct — you don't want an optional dependency taking down an app that doesn't strictly need it. But graceful degradation only helps the next person if the degraded state is at least as visible as a crash would have been. A stack trace gets noticed. A print statement that reads almost exactly like the six lines around it does not.
We only caught this because of one habit: after every deploy, read the actual startup log, line by line, rather than trusting that "container healthy" and "build succeeded" add up to "feature working." They don't. They're answering three different questions, and only one line in the whole log was answering the fourth.
The fix, and the one we're still owed
The immediate fix was mechanical: add the missing package to the image, rebuild, redeploy, confirm the startup log now shows the feed actually connecting. Ten minutes, once we knew what to look for.
The fix we still owe the codebase is the more important one: that print statement should not read like routine startup chatter. A feature failing to start needs to look different in the log than a feature starting normally — a distinct prefix, a warning level, something a future deploy can't scroll past without noticing. We wrote defensive code and stopped one step short of making the defense visible.
What I'd tell another team
You can see the feature itself now live on our sample fleet-tracking demos — real vessel positions where they're actually present, an honestly-labeled sample where they're not. If you want a team that reads the log instead of the exit code, say hello.
More from BlueWave
RoomPlan vs Matterport vs Polycam: which one belongs in your contractor's toolkit?
8 min
Hawaii complianceHawaii GET tax for contractors: how the §237-13(3)(B) sub-deduction actually works
6 min
WorkflowHow to scope a renovation in 60 seconds (and why your hand-written estimate keeps losing jobs)
5 min