Self-hosted service
clemcer/LoggiFly avatar
clemcer/LoggiFly

LoggiFly: alerting on Docker log patterns, and acting on them

Get Alerts from your Docker Container Logs

1,781 stars51 forksPythonMIT

At a glance

What is it?
LoggiFly watches container logs for keywords and regular expressions, sends the matching lines to Ntfy or any Apprise supported channel, and can stop or restart the container that produced them. It is small, it is MIT licensed, and the config reload behaviour makes it usable without a restart.
Who is it for?
LoggiFly fits a homelab or small production host where a single container emits a line that should reach a phone, and where an automatic restart on a specific fatal error is acceptable behaviour. It does not fit a fleet that already has a log pipeline, since it reads the Docker log stream directly rather than ingesting from a collector, and it is the wrong tool where the keyword is ambiguous, because the action it can take is drastic.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The gap between a container that is up and a container that is working

LoggiFly is described as a lightweight tool that monitors Docker container logs for predefined keywords or regex patterns and sends notifications. The problem it addresses is not that containers crash, which every orchestrator already tells you about, but that a container keeps running while doing the wrong thing.

The README's own examples are the argument for it. Catching security breaches, such as failed logins in Vaultwarden, is the first case named. The second is debugging crashes with attached log context, where the notification carries the log file rather than just a sentence about it. The third is the one that goes further: restarting containers on specific errors, or stopping them completely to avoid restart loops. The fourth is monitoring custom application behaviour, such as knowing when a user downloads an audiobook on an Audiobookshelf server.

That last pair of cases is where a log watcher earns its place. A restart loop is a container that is technically healthy to the orchestrator and functionally dead, and a user-facing action that should have been an alert is invisible unless something reads the stream.

The audience is therefore whoever runs the Docker host, which in practice is a self hosted homelab, a small server, or a single node cluster. The topics on the repository point the same way: apprise, docker, docker-logs, ntfy, server-events, notifications and monitoring.

The config file is a Pydantic model exported to YAML

The interesting engineering is in how the configuration is defined and kept in sync with the documentation, because there is no static config schema file in the repository. The schema is generated.

The Python side uses pydantic, and the repository has a script at scripts/export_schema.py that runs before the documentation build. The package.json scripts make the order explicit, with a predocs:dev step that calls the export script before vitepress dev docs --host, and a predocs:build that does the same before vitepress build docs. The documentation site is therefore rendered from a schema that comes out of the code, so a change to the Pydantic model and a change to the reference cannot drift apart.

The JavaScript side of that pipeline is thin. The only runtime dependency is js-yaml, and the dev dependency is vitepress. The application itself is Python, and the requirements file pins docker, requests, pyyaml, watchdog, apprise, pydantic and Jinja2 at exact versions.

What that stack implies is worth stating. pyyaml parses config.yaml, pydantic validates the parsed values against the models, Jinja2 renders the templates for notification bodies and titles, watchdog watches the config file for changes, the docker SDK talks to the daemon, requests handles plain HTTP, and apprise handles the notification fan-out. Every one of those choices is visible in the requirements file, which makes the behaviour of the tool fairly easy to reason about before you install it.

One alert channel or a hundred, through the same call

Notification delivery goes through Ntfy or Apprise, and the README describes the second as the general case: send notifications directly to Ntfy, or via Apprise to more than a hundred different services, naming Slack, Discord and Telegram, or even your own custom endpoint.

That choice has a practical consequence. If you use Ntfy directly, the tool is talking to one specific service and the configuration is simple. If you use Apprise, the same tool can talk to whatever you already use. The tool itself does not change, which is the point: the alert format, the filtering and the templates stay the same whichever channel carries them.

The message content is also configurable in two layers, and the README lists both. You can filter log lines for relevant information, and you can use templates for your messages and notification titles. The filtering decides which lines are worth sending, and the template decides how they read once they arrive. Splitting those two is the right design, because the filter is about noise and the template is about the recipient, and a phone notification and a chat message want the same facts in different shapes.

For debugging, the tool can attach a log file to the notification, which is the mechanism behind the README's promise of alerts with attached log context.

The stop and restart trigger is a blunt instrument on purpose

The feature that makes LoggiFly different from a plain alerting tool is the ability to trigger a stop or a restart of the monitored container when a specific pattern is matched. The README lists it as trigger stop and restart, and the ideal-for list says a restart or stop of the monitored container can be triggered on specific critical keywords.

This is worth thinking about carefully, because it is the point where a monitoring tool becomes a control system. The pattern that matches is a string in a log stream, and a string in a log stream is not a reliable signal of state. A container that logs the word panic in a caught exception is different from a container that logs panic and dies, and a regex that is too broad will match a line the author never meant as an error.

The stop option has a different purpose from the restart option. Stopping a container that is stuck in a crash loop prevents the loop from filling the disk and burning the CPU, and it is a deliberate way to take a broken service offline rather than let it thrash. Restarting is for the case where the container is genuinely wedged but comes back clean.

The safe way to configure this is to start with the alerting side only, watch what the patterns actually match for a few days, and only then wire the action. The configuration reload described below makes that iteration cheap, since a change to the pattern takes effect without restarting the tool.

The config is reloaded the moment you edit it

The feature list says the program automatically reloads the config.yaml when it detects that the file has been changed. The watchdog dependency in the requirements file is the implementation of that.

Why this matters is that it changes the operational model. In a tool that only reads its configuration at startup, every change to a pattern or a template means a restart, which means a moment when alerts are not being sent. Here, the edit is a file write, the running process picks it up, and there is no gap. The same argument applies to the templates, so you can iterate on how an alert reads without stopping the monitor.

The documentation links point to a getting started page and a configuration page, both on the same static site, and the site is built with VitePress from the docs directory. The homepage of the project is that documentation site.

There is a second, less obvious reason for the reload to exist, and it is the template pipeline. Because the alert body is rendered from a template with Jinja2, the interesting content of an alert is not a fixed string but the matched line plus whatever context you choose to include. Being able to change that rendering while the tool is running is what makes that practical to tune.

Running it as a distroless container with an entrypoint of python app.py

The Dockerfile in the repository is a two stage build that ends in a distroless image, and it is worth reading in full because the details are unusual.

The build stage installs the pinned dependencies into a prefix directory, and the final stage copies them across:

dockerfile
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
dockerfile
FROM gcr.io/distroless/python3-debian13

WORKDIR /app
# USER 1000:1000

ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONPATH=/usr/local/lib/python3.13/site-packages

ENTRYPOINT ["python", "app.py"]

The final image contains no shell and no package manager, which is the point of a distroless base, and the application code is copied in from the app directory in the repository root. Two environment settings matter in practice. PYTHONUNBUFFERED=1 stops Python from buffering stdout, which is exactly what you want in a container whose output is collected as logs, and PYTHONDONTWRITEBYTECODE=1 keeps the runtime from writing .pyc files into the image.

The line you should not skip is the commented one. The Dockerfile has # USER 1000:1000 with the hash in front, so the process runs as root inside the container by default. For a tool whose job is to talk to the Docker socket and to stop and restart containers, running as root inside the container is not automatically a problem, since the Docker socket is what carries the authority, but it is a decision to make rather than a default to inherit. Setting that line to USER 1000:1000 and making sure the app directory is writable by that user is a small change.

The image is labelled with its source repository, and the requirements are pinned to exact versions, which makes a rebuild reproducible.

MIT licensed, three releases this year, and a docs pipeline in JavaScript

The licence is MIT, with the LICENSE file at the root of the repository, so there is no obligation beyond keeping the notice if you deploy it as part of a service.

The release cadence in 2026 has been regular. Version 2.0.1 was published on 2026-05-18, 2.1.0 on 2026-07-02 and 2.1.1 on 2026-07-15, and the last push to the repository was on 2026-10-14. The project is small enough to read in an afternoon, and the documentation site is published from the same repository rather than from a separate docs project.

The one structural curiosity is the mixed language toolchain. The application is Python, and the documentation build is JavaScript with VitePress and js-yaml, with a Python script bridging the two by exporting the schema before the docs build runs. It is more moving parts than a single-language project needs, and the reason is that VitePress is the docs engine, which is where the schema file has to land.

The upgrade path is straightforward in practice. The pinned dependency versions in requirements.txt are the thing most likely to need attention over time, since a fixed docker SDK and a fixed apprise will eventually need refreshing, and the rest of the tool is stable enough that a config change is the only thing most operators will ever need to make.

Editorial conclusion

LoggiFly fits a homelab or small production host where a single container emits a line that should reach a phone, and where an automatic restart on a specific fatal error is acceptable behaviour. It does not fit a fleet that already has a log pipeline, since it reads the Docker log stream directly rather than ingesting from a collector, and it is the wrong tool where the keyword is ambiguous, because the action it can take is drastic. Before you enable the stop and restart trigger, write down exactly which pattern maps to which container, and read the configuration reference at clemcer.github.io/loggifly/guide/config, since the shipped image runs as root unless you change it.

Frequently asked questions

What does LoggiFly do with Docker logs?

It monitors container logs for predefined keywords or regex patterns, including patterns that span multiple lines, and sends notifications with the matching content. It can also trigger a stop or a restart of the monitored container, and attach a log file to the notification for context.

Which notification channels does LoggiFly support?

It can send notifications directly to Ntfy, or through Apprise to more than a hundred services, including Slack, Discord and Telegram, or to your own custom endpoint.

Does LoggiFly need to be restarted when I change its configuration?

No. The README states the program automatically reloads config.yaml when it detects that the file has been changed, and the watchdog package in the requirements file is what performs that check.

Can LoggiFly monitor containers on remote Docker hosts?

Yes. Remote hosts are listed as a feature, so the tool can connect to multiple remote Docker hosts rather than only the local daemon. The feature list also names Docker Swarm and Podman as supported platforms.

How is the LoggiFly container image built?

The Dockerfile installs the pinned requirements into a prefix in a build stage and copies them into a distroless python3-debian13 final image, with the app directory copied in and an entrypoint of python app.py. The USER 1000:1000 line is present but commented out, so the container runs as root unless you change it.

Official sources

  1. clemcer/LoggiFly on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/clemcer-loggifly.svg)](https://hysenlabs.com/projects/clemcer-loggifly)

Was this useful?