{"slug": "a-container-restarted-39352-times-every-exit-code-was-0", "title": "A container restarted 39,352 times. Every exit code was 0.", "summary": "A developer traced a container that had restarted 39,352 times with every exit code reporting 0, caused by an MCP server running in stdio mode without stdin_open or tty set in its Docker Compose configuration. The process read stdin, hit EOF, exited cleanly, and was revived by restart: unless-stopped in an infinite loop that crash-based monitoring never caught. The same investigation found two other containers falsely marked unhealthy for four weeks because Node 18's DNS resolution sent localhost health checks to ::1 against IPv4-only services, leading the developer to argue restart count is a more reliable signal than exit codes or health checks.", "body_md": "A container on one of my servers had restarted **39,352 times**. Every single exit code was `0`.\n\nThat number is why nobody noticed. Monitoring watches for crashes, and this was not a crash. The process started, found nothing to do, exited *successfully* — and `restart: unless-stopped` dutifully brought it back. About 39,000 times.\n\nThis turned into one of those debugging sessions where every layer is behaving \"correctly\" and the system as a whole is broken. Here's the walk-through, and the one metric I now trust more than exit codes.\n\nThe service was an MCP server running in **stdio mode**. In stdio mode the process talks over standard input/output — it reads requests from `stdin` and writes responses to `stdout`.\n\nHere's the compose block, simplified:\n\n```\nservices:\n  mcp:\n    image: my/mcp-server\n    restart: unless-stopped\n    # note what's NOT here:\n    # stdin_open: true\n    # tty: true\n```\n\nWithout `stdin_open` (the compose equivalent of `docker run -i`), the container gets **no open standard input**. So the MCP server boots, reads from `stdin`, and immediately hits **EOF**. There's nothing to read and never will be. It does the correct thing on EOF: it shuts down cleanly and exits `0`.\n\n`restart: unless-stopped` sees a stopped container and restarts it. The new process boots, reads `stdin`, hits EOF, exits `0`. Restart. Boot. EOF. Exit `0`. Restart.\n\nEvery individual decision in that loop is right. The process *should* exit when its input stream closes. The restart policy *should* revive a service that stopped without being told to. Compose it together and you get a clean, successful, infinite loop that a crash-based alert will never fire on.\n\nThe fix is one line — give it a stdin to hold open:\n\n```\n    stdin_open: true\n    tty: true\n```\n\nBut the fix isn't the interesting part. The interesting part is why it stayed invisible for 39,352 iterations.\n\nWhile I was in there, I looked at the other containers on the same box. Two of them had been marked `unhealthy` for **four weeks**. Both were completely fine the whole time.\n\n`localhost` from a Node 18 process. Since Node 17, Node no longer reorders DNS results, so `::1` first`ECONNREFUSED`, forever — against a service that was answering fine on `127.0.0.1`.\n\n``` js\n// Node 18+: this can resolve to ::1 and miss an IPv4-only server\nconst res = await fetch(\"http://localhost:3000/health\");\n\n// Force IPv4 if that's what your service binds:\nconst res = await fetch(\"http://127.0.0.1:3000/health\");\n```\n\nSo on one machine, at the same time:\n\n`unhealthy`` exit 0`\nThe health signal wasn't just wrong. It was inverted in both directions at once. If I'd trusted it, I'd have \"fixed\" the two healthy ones and never looked at the broken one.\n\n`stdin_open: true` + `tty: true` for the stdio service.`docker run`, fails in a health check\" as an IPv4/IPv6 smell).\nThat last one is the takeaway.\n\nExit `0` only tells you the process *agreed to leave*. It does not tell you it *should have*.\n\nRestart count, by contrast, is honest. A number that climbs into the thousands means something is wrong regardless of how politely each instance exited. It's a better smoke alarm than exit code, and a far better one than a health check that can be pointed at the wrong port or the wrong IP stack.\n\nCheap checks worth stealing:\n\n`stdin_open` will exit cleanly on boot. \"Clean\" is not the same as \"correct.\"\nWhat's the longest a silent restart loop has run in your infrastructure before anyone noticed?", "url": "https://wpnews.pro/news/a-container-restarted-39352-times-every-exit-code-was-0", "canonical_source": "https://dev.to/cxtao/a-container-restarted-39352-times-every-exit-code-was-0-2g6i", "published_at": "2026-09-19 07:05:01+00:00", "updated_at": "2026-09-19 07:54:36.221156+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "developer-tools", "mlops"], "entities": ["Docker", "Docker Compose", "Node.js", "MCP"], "alternates": {"html": "https://wpnews.pro/news/a-container-restarted-39352-times-every-exit-code-was-0", "markdown": "https://wpnews.pro/news/a-container-restarted-39352-times-every-exit-code-was-0.md", "text": "https://wpnews.pro/news/a-container-restarted-39352-times-every-exit-code-was-0.txt", "jsonld": "https://wpnews.pro/news/a-container-restarted-39352-times-every-exit-code-was-0.jsonld"}}