Skip to content
Hack Your WorldSoftware · Infrastructure · Home automation

Analysis

A Running Container Is Not a Deployment

Three running server modules with broken ownership, reachability, and session boundaries
AI image: Hack Your World

A running container proves that one process started. It does not prove external reachability, a supported topology, durable state, client continuity, backup, monitoring, or rollback. Those properties belong to the deployment around the container.

“Running” is one useful fact, not the verdict

docker ps can tell me that the main process is still alive. A restart policy can bring that process back after it exits. Neither fact proves that a published port reaches the listener, an authenticated request succeeds, persistent data is recoverable, or a client can reconnect after replacement.

Docker’s own documentation treats these as separate concerns. A restart policy controls whether a container starts again. Compose can wait for a dependency marked service_healthy, but only if the health check measures something useful. A Docker volume persists outside an individual container’s lifecycle, but persistence is not the same as a tested backup.

I learned the distinction through three repositories I maintain. Each failure passed a shallow version of “is the container up?”

Failure one: Compose owned only half the Home Assistant stack

In April 2026, I updated an old Hass.io Compose repository. I pinned a current Supervisor image, moved the registry reference, added restart: unless-stopped, and wrote a helper that could wait for port 8123. The file became easier to run.

The architecture was still wrong. Compose launched one privileged Supervisor container with the host Docker socket and D-Bus mounted into it. Supervisor then created Home Assistant and app containers outside Compose’s ownership. I had two control planes: Compose knew about Supervisor, and Supervisor knew about the children it created.

privileged: true
restart: unless-stopped
volumes:
  - /var/run/docker.sock:/var/run/docker.sock
  - /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket

A clean docker compose up did not give me one lifecycle. Stopping the Compose project did not necessarily describe the state of every child. Removing a child manually could leave Supervisor’s model out of step with Docker. An image-family change that looked correct for x86 also failed because Supervisor expected a private :landingpage tag contract.

The larger problem was support. Home Assistant ended support for the Supervised installation method with the 2025.12 release. Its current installation choices are Home Assistant OS and Home Assistant Container. A tidier Compose file could not turn my custom Supervisor arrangement into a supported installation.

That changed my deployment decision. If I want Supervisor and managed apps, I use Home Assistant OS. If I want to own the Linux host and neighboring containers, I use Home Assistant Container and accept that operational work. I retired the old stack because ownership and support are part of deployment, not documentation added later.

Failure two: the MediaWiki MCP container listened in the wrong place

The MediaWiki MCP server had a more ordinary Docker failure. Its HTTP server defaulted to localhost. Inside the Alpine-based image, that name resolved to IPv6 ::1. The process started and kept running, but the published IPv4 port reached no listener.

A careless health check could have made the same mistake from the opposite direction. A probe that calls whichever address the process happens to use may prove only that code inside the same network namespace can talk to itself. It does not prove that Docker’s published port, host firewall, reverse proxy, DNS, TLS, and authentication path work.

I made the container’s listening contract explicit:

ENV MEDIAWIKI_MCP_HOST=0.0.0.0

HEALTHCHECK --interval=30s --timeout=10s \
  --start-period=10s --retries=3 \
  CMD wget -qO- http://127.0.0.1:8009/health || exit 1

0.0.0.0 lets the service accept traffic on the container interfaces. The numeric loopback address makes the internal liveness probe deterministic. Those are two different checks. I still need an outside-in request through the route clients actually use before I call the service reachable.

This is why a green container health status is evidence, but not enough evidence. The probe has to be read like a test: what exact failure can it detect, and which important failures remain invisible to it?

Failure three: the GitLab MCP process restarted without its sessions

The GitLab MCP server exposed a failure that no image build or port check could catch. Its HTTP sessions lived in memory. After a deployment or restart, the process returned, but clients could keep sending the session IDs issued by the previous process.

The broken behavior tried to treat a stale-ID request like a chance to create a fresh transport. That grafted old client state onto a new server lifecycle. The correct recovery contract was simpler: an unknown session ID returns HTTP 404, and a compliant client initializes a new session.

I also fixed a recursive close path. The MCP SDK could turn server.close() into transport.close(), which invoked the same close handler again. The teardown now removes the session from the map before closing the server, so the re-entrant callback becomes a no-op.

The targeted session tests passed when I published the detailed GitLab MCP case study. I did not rerun that suite for this article. The useful lesson here is narrower: restarting a process does not preserve an in-memory protocol, and a deployment needs an explicit client recovery response.

A development container solves a different problem

A development container can still be valuable. It can pin the compiler, package manager, system libraries, editor extensions, and setup commands needed to move a checkout from clone to test. That is a contract with the project.

Production reachability, live authentication, backups, schema rollback, and client behavior across a restart remain separate tests. I keep the development environment reproducible because it shortens the path to a valid build and test run. I do not promote that convenience into production evidence.

A production image needs the same treatment. Reproducing the filesystem and entrypoint is useful. It says nothing by itself about the services, secrets, storage, network, certificates, and operators around that image.

Persistent is not the same as recoverable

Docker documents that a volume’s contents live outside the lifecycle of a container. That is exactly why volumes are useful. It is also why recreating a container does not test recovery: the new process may simply remount the same intact data.

My home-lab Ansible repository separates three directions instead of pretending they are one:

  • apply selected configuration to a system;
  • pull selected text configuration into a reviewable snapshot;
  • restore one Home Assistant file through a deliberately narrow path.

The snapshot excludes databases, logs, caches, media, credentials, private keys, environment files, authentication databases, and generated backups. It uses delete: false, so a removed remote file does not immediately erase the older local copy. That favors recovery, but it also means the snapshot can contain stale state.

I found two gaps while reviewing that code: the documented maximum-size cap is not enforced, and the scheduled wrapper stages fewer destination roots than the snapshot playbook can populate. Those gaps do not make Docker volumes bad. They show why “the data is mounted” and “I can restore the service” are different statements.

The deployment checklist I use now

What must be true beyond a running container
Boundary Question I need to answer Evidence I want
Process Did the intended process start and stay alive? Exit status, logs, and a bounded liveness check
Reachability Can a real client reach the published route? An outside-in request through the actual proxy, TLS, and auth path
Health Does the probe measure useful service behavior? A check tied to the dependency or capability it claims to cover
State What survives replacement, and who owns it? Named storage, documented exclusions, retention, and restore test
Restart What happens to clients and in-memory state? Expired-state response and a tested reinitialization path
Update How does the image, configuration, and schema move forward? Pinned input, migration order, compatibility check, and observed result
Rollback Can I return to a known state without inventing commands during an incident? Versioned artifact, compatible data, and a rehearsed reversal
Support Is this architecture supported by the project I depend on? Current upstream installation and lifecycle documentation

What I require before calling a container deployed

I want one owner for the lifecycle. I want the listener address and published route to be intentional. I want an internal health check and at least one external check, with their different limits written down. I want persistent paths inventoried, secrets excluded from casual snapshots, and a restore that has been tried somewhere safe.

I also want replacement behavior. If the service is stateless, a new instance should be able to accept the next request. If it keeps sessions, clients need a clear expired-state response. If an update changes data, the migration and rollback path has to be known before the old instance disappears.

Compose can encode part of that system. Docker can enforce part of it. Neither tool can decide support policy, define the right health assertion, or prove recovery on my behalf.

The three deployments I checked

The platform behavior is cross-checked against Docker’s official documentation for volume lifecycle and backup, Compose dependency health, and restart policies. Home Assistant’s current installation page and Supervised deprecation announcement support that installation choice.

The concrete failures come from repositories I maintain: the historical Hass.io Compose stack, MediaWiki MCP, and GitLab MCP, plus the private home-lab snapshot source described in the Ansible article. The detailed Home Assistant, MediaWiki MCP, and GitLab MCP articles preserve their individual limits of the test.

I reviewed those current source copies and official documents for this replacement. I did not deploy the containers, restart a live service, run the repository test suites, execute the private Ansible playbooks, or perform a restore for this article. Prior test results are identified as prior results rather than a fresh run.