Skip to content

Healthy Container, Dead Browser: A Compose Migration Trap

Adityo Guni Waluyo

Five hours of 'healthy' while the browser inside the container had been dead since minute one. Notes on user, HOME, and shallow healthchecks.

<!-- qwen-article.py | 2026-09-11T18:50:21 | chat: -->

1. Judul (H1) Meta (untuk sistem): A personal dev blog post about debugging a Docker container that reports healthy while the internal browser crashes due to UID and HOME folder ownership mismatches. Slug (untuk sistem): healthy-container-dead-browser

Today I ran my custom 5-layer preflight tool, camofox-doctor, against a newly migrated Docker Compose setup for my anti-detect browser server combined with a local GPU-accelerated language model. Layer 2 smoke test immediately failed. The open_tab endpoint threw an HTTP 503 three times in a row. My first guess was a transient crash during startup. I restarted the container and waited. five hours later, the orchestration dashboard still showed the service as perfectly fine. The container was completely silent while the actual application inside was dead. This massive discrepancy between the dashboard status and the actual application state sent me down a rabbit hole — questioning my entire deployment strategy.

Digging Into The Silent Failure I pulled the logs to see what actually happened during that silent period. The output explicitly stated that the browser engine refuses to launch if the home folder isn't owned by the current user, a trap well known in the Playwright ecosystem [1]. This strict security behavior has been deliberate since Firefox 61 [4]. Running ps aux inside the running instance revealed the main process was running as root. However, the internal home directory was strictly owned by a non-root user with uid 1000. Compose runs the container process as root when no user override is defined [2], and this mismatch is exactly the trap of migrating manual setups without making the execution context explicit. The healthcheck only performed a shallow TCP port check [3], completely ignoring the broken application state. It just verified that the Node server was listening, totally blind to the fact that the underlying engine had crashed on initialization. I realized my orchestration tool was lying to me about the actual health of the service, masking a critical failure behind a passing network probe.

Chasing The Ownership Ghost Fixing the user directive seemed obvious at first glance. I added the user parameter to my configuration to force execution as the correct owner. This immediately changed the error to an internal server error complaining about missing version information in an empty cache folder. The browser engine resolves its binary cache via the home environment variable, which was now pointing to a non-existent directory for that specific user context. The final fix required exactly two lines in the configuration file: force the uid, and point the home environment variable back to the directory the image actually prepared:

Sources

Related articles