What this is about
Container management UIs like Portainer or Arcane make day-to-day Docker work easier: starting containers, checking logs, updating images. But for pure status monitoring, they're just a window onto the current moment. Switching tools often changes not just the interface, but also how (and whether) states get logged at all.
Building your own pipeline solves this problem at the root. It queries the Docker daemon directly, regardless of which management tool happens to be in use. That makes you independent of any individual tool's feature set and release cadence, and gives you a continuous history that survives a tool switch.
The main benefit is continuity. While you swap out the user interface, the underlying data for alerting and analysis stays stable. That reduces the risk of an outage going unnoticed during the migration phase.
Requirements
Before setting up a monitoring pipeline, it's worth taking stock of what you actually need. First, you need access to the Docker socket or a comparable API to read container status, health checks, and restarts. This access should work independently of whichever management tool you happen to be running.
Second, you need somewhere to store the historical data. For small to medium setups, a local SQLite database is entirely sufficient. It doesn't need a separate server process and is easy to back up or migrate.
Third, you need a mechanism that detects deviations and triggers alerts. That can be simple threshold logic (container not "running" for X minutes) or classification via a local model, say through Ollama, if you want to semantically categorize log text. For plain status changes, the simple logic is usually enough.
Fourth, you need a notification channel. ntfy, Telegram, or email all work fine for small setups. What matters is that the channel delivers reliably and that you can mute alerts when needed without stopping the pipeline itself.
Fifth, you should plan for a heartbeat. A daily check confirming that the pipeline itself is still running prevents a silent collector failure from going unnoticed.
Comparing your options
There are essentially three ways to monitor container status. The first option is to rely entirely on whichever management tool you're using. That's the least effort, but it ties you to that tool's feature set and release cycle. When you switch tools, you're basically starting your monitoring from scratch again.
The second option is a dedicated monitoring solution like Prometheus with cAdvisor or node-exporter. That's robust and scales well, but it adds extra components and operational overhead that often isn't worth it for small setups.
The third option is a lean, self-written pipeline, like the one described in this article. It's deliberately kept minimal: a collector script, an SQLite database, simple alert logic, and a notification channel. The advantage is full control over your data structure and alert rules. The downside is that you're on your own for maintenance and future development.
For most indie setups and smaller teams, the third option is a solid compromise. It stays small enough to understand in an afternoon, yet still delivers a reliable history.
Implementation
The example below shows a collector that queries the status of running containers, logs it to SQLite, and fires an ntfy notification whenever the status changes. The code uses the official Docker Python library and the standard sqlite3 module.
"""Collector fuer Container-Status, Speicherung und Alarmierung."""
from __future__ import annotations
import logging
import sqlite3
from pathlib import Path
import docker
import requests
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("container_watch")
DB_PATH = Path("container_status.db")
NTFY_TOPIC = "https://ntfy.sh/container-alerts"
def init_db(db_path: Path) -> sqlite3.Connection:
"""Legt die Statustabelle an, falls sie fehlt."""
conn = sqlite3.connect(db_path)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS container_status (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
status TEXT NOT NULL,
checked_at TEXT DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.commit()
return conn
def get_last_status(conn: sqlite3.Connection, name: str) -> str | None:
"""Liefert den zuletzt gespeicherten Status eines Containers."""
row = conn.execute(
"""
SELECT status FROM container_status
WHERE name = ?
ORDER BY id DESC LIMIT 1
""",
(name,),
).fetchone()
return row[0] if row else None
def notify(message: str) -> None:
"""Sendet eine Benachrichtigung ueber ntfy."""
try:
requests.post(NTFY_TOPIC, data=message.encode("utf-8"), timeout=5)
except requests.RequestException as exc:
logger.warning("Benachrichtigung fehlgeschlagen: %s", exc)
def collect_and_check() -> None:
"""Fragt Containerstatus ab und loest Alarme bei Aenderung aus."""
client: docker.DockerClient | None = None
conn: sqlite3.Connection = init_db(DB_PATH)
try:
client = docker.from_env()
containers = client.containers.list(all=True)
for container in containers:
name = container.name
status = container.status
previous = get_last_status(conn, name)
conn.execute(
"INSERT INTO container_status (name, status) VALUES (?, ?)",
(name, status),
)
conn.commit()
if previous is not None and previous != status:
notify(f"Container {name}: {previous} -> {status}")
logger.info("Statuswechsel bei %s: %s -> %s",
name, previous, status)
except docker.errors.DockerException as exc:
logger.error("Docker-API nicht erreichbar: %s", exc)
finally:
if client is not None:
client.close()
conn.close()
if __name__ == "__main__":
collect_and_check()
You can run this script at fixed intervals, say every five minutes, via a systemd timer or a cron job. For the daily heartbeat, a simple additional job that checks whether any entries were written in the last 24 hours, and fires its own alert if nothing happened, is enough.
For more complex classification, such as categorizing error text from logs, you can extend the collector with a call to a local model via Ollama. For plain status changes, as in the example, that's usually not necessary though.
Common pitfalls
A common mistake is exposing the Docker socket to the collector unprotected. Make sure access to the socket is restricted to the user running the script, and avoid unnecessary root privileges.
Another pitfall is skipping a test phase when switching management tools. Running the old and new solution in parallel for a limited time shows whether your own pipeline detects the same states as the previous tool did.
The alert threshold also deserves attention. Containers that restart regularly (during rolling updates, for example) will otherwise trigger constant false alarms. A short delay or a counter for consecutive status changes cuts this alert noise down considerably.
Finally, the SQLite file often gets forgotten when container hosts get rebuilt. Back up the database file regularly, for instance using the same backup mechanism you use for other configuration data.
When it's worth it (and when it isn't)
Building your own watch-and-alert pipeline pays off if you run multiple Docker hosts, switch management tools regularly, or need a continuous history for audits. With the building blocks shown here, the effort for the collector, SQLite storage, and notifications is low and easy to maintain.
If you're only running a single host with a handful of containers and the status display in your management tool is good enough for you, the extra effort barely pays off. In that case, the tool's built-in monitoring is usually completely sufficient.
For teams with greater scaling and dashboarding needs, a dedicated monitoring solution like Prometheus is the better choice. The lean pipeline shown here is best suited for small to medium setups that want to stay independent of any particular management tool.