Dozzle: Docker Logs Without the Terminal Gymnastics
A live log viewer with no database and no regrets

Contents
Something is broken. You know roughly which container. You ssh in, run docker ps, squint at the truncated names, copy one, run docker logs -f --tail 200 that-container, realise the interesting thing is in a different container, Ctrl-C, scroll back up to find the name, and start again. Then you need both at once, so you open a second terminal. Then it turns out the problem is on a different host entirely.
This is a solved problem and the solution costs 20 MB of RAM.
Dozzle is a web-based live log viewer for Docker. It reads the Docker socket, streams container logs to a browser, and stores absolutely nothing. There is no database, no index, no retention policy and no ingestion pipeline. It shows you what Docker already has, which is exactly what docker logs shows you, arranged so a human can use it.
The design decision that makes it work
Dozzle’s defining choice is that it is stateless. It does not collect logs. It does not ship them anywhere. When you open a container in the UI, Dozzle calls the Docker API’s log-stream endpoint and pipes the result to your browser over a server-sent-events connection. Close the tab and the stream ends. Restart Dozzle and nothing is lost, because nothing was ever held.
This sounds like a limitation and it is the entire point. Every log aggregation system — and I say this as someone running Loki quite happily — costs you a collector on every host, a storage backend, a retention decision, a disk budget and a label schema you will get wrong the first time. Dozzle costs a container.
The corollary is honest and you should hold it firmly: Dozzle can only show you what Docker still has. If your logging driver is json-file with max-size: 10m, Dozzle can show you the last 10 MB and no more. If a container is recreated, its logs go with it. Dozzle is a window, and the window only looks at what is currently there.
Which is fine, because 90% of the time the thing you want is what happened in the last four minutes.
Getting it running
| |
That is the whole install. Open it and you have a sidebar of running containers with live search, a log pane that colourises levels, and a real-time stream that starts the moment you click.
DOZZLE_FILTER is worth setting from day one on a busy host. Without it, Dozzle lists every container including the six infrastructure ones you never look at. With a label filter, only the containers you have opted in appear:
| |
You can filter by name, status or label. I use a label because names change and labels are declarative.
Now, the important part. That volume line mounts the Docker socket. :ro on a socket does almost nothing useful — the socket is an API endpoint rather than a file, and read-only affects the filesystem permission on the socket node, which is not where the danger lives. Anything that can talk to the Docker socket can start a container mounting the host’s root filesystem, which is root on the host, full stop.
Dozzle only calls read endpoints. That is a property of Dozzle’s code rather than a property enforced by anything, and the mitigation, if you want one, is a socket proxy that whitelists the container-list and container-logs endpoints and rejects everything else. That is a real defence and I would use it on anything internet-adjacent.
Agent mode: the feature that changes the calculus
Running Dozzle on one host is nice. The version that made me keep it is agent mode, which puts all your hosts in one sidebar.
On each remote host:
| |
On the hub:
| |
One sidebar, every container, every host, live. The hub-to-agent link is mTLS-secured rather than a plain socket exposure, which matters enormously — the older approach to multi-host Dozzle was DOZZLE_REMOTE_HOST pointing at a Docker API on tcp://host:2375, and exposing an unauthenticated Docker API on your LAN is one of the most effective ways to hand over your infrastructure. Agent mode exists specifically so nobody has to do that. Use it.
The agent is the same image with a different command, which is a packaging decision I wish more projects made. One image to pull, one thing to update.
Actually using it
Three features do the work.
Split view. Click one container, then shift-click another, and they render side by side with a shared timeline. The reverse proxy and the app it fronts, next to each other, live, while you reproduce the bug. This is the feature. I have watched a request fail in Traefik’s log and appear malformed in the backend’s log in the same second, and understood the problem instantly. Doing that with two terminals and docker logs -f is technically possible and practically miserable.
Search that is actually live. The search box filters the stream as it arrives, with regex support, and it highlights matches. Typing (?i)error|warn narrows a firehose to the interesting parts without stopping the stream. Because it is client-side over the live stream, there is no query latency at all.
The container list search. Press Ctrl-K, type three letters, hit enter. On a host with forty containers this is the difference between a tool you use and a tool you have installed.
A few smaller things that add up. Dozzle detects JSON log lines and pretty-prints them with collapsible fields, which turns a wall of structured-logging soup into something readable. It auto-detects log levels from common formats and colours them. It has a download button that gives you the current buffer as a text file for pasting into a bug report. And DOZZLE_LEVEL sets a default minimum severity so the sidebar’s “has errors” indicators mean something.
Dozzle also has an actions feature — start, stop and restart containers from the UI — which is disabled by default and which I leave that way. A log viewer that can restart my database is a log viewer I have to think about the auth on. Enable it with DOZZLE_ENABLE_ACTIONS: "true" if you disagree, and put real authentication in front of it if you do.
Authentication
Dozzle ships with no auth at all by default, which is correct for a tool bound to localhost and wrong for anything else. Your container logs contain connection strings, tokens people accidentally logged, internal hostnames and everything else your applications thought was harmless to print.
Two options. The simple provider, with a users file:
| |
| |
Generate the hash with htpasswd -bnBC 11 "" yourpassword | tr -d ':\n' and keep the file out of git.
Or hand it to your existing single sign-on with DOZZLE_AUTH_PROVIDER: forward-proxy, at which point Dozzle trusts the Remote-User and Remote-Email headers your proxy sets. This is the right answer if you already run an SSO, and it is a catastrophic answer if anything can reach Dozzle without passing through that proxy first — those headers are trusted unconditionally. Bind to localhost, proxy over the loopback, and the problem disappears.
Understanding the buffer you are looking at
Since Dozzle is a window onto Docker’s own log storage, it pays to know what is behind the glass.
With the default json-file driver, Docker writes every line your container sends to stdout or stderr into a JSON file on the host, one object per line:
| |
Three facts fall out of that, and each one explains a thing people find surprising.
The JSON wrapper costs you. Every line carries a timestamp, a stream name and JSON escaping, which on short log lines is easily 60% overhead. Your 50 MB cap holds rather less than 50 MB of actual log.
Rotation is per-container and silent. max-file: 3 with max-size: 50m means each container can hold 150 MB, and forty containers means 6 GB of logs you never decided to keep. Multiply before you set the numbers.
The local driver is better and nobody uses it. It stores the same data in a compressed binary format, roughly halving the disk for the same retention, and docker logs and therefore Dozzle read it identically. The only reason json-file remains the default is that third-party tools sometimes read the files directly. If nothing on your host does that, switch:
| |
Restart the daemon and new containers pick it up. Existing containers keep their old driver until recreated, which is the usual Docker rule and the usual source of “it did not work”.
Troubleshooting
Empty container list. The socket is not mounted, or the socket’s group ID inside the container does not match the host’s docker group. Check with docker exec dozzle ls -l /var/run/docker.sock. On a rootless Docker install the socket lives under $XDG_RUNTIME_DIR and you must mount that path instead.
Logs appear for some containers and not others. The logging driver. Dozzle can only read from json-file and local, because those are the only drivers whose logs Docker itself retains and serves through the API. If you set logging.driver: syslog or journald or gelf on a container, docker logs returns nothing for it and so does Dozzle. This catches people who configured a shipping driver for their aggregation stack and then wondered why the viewer went blank.
Only the last few lines are available. Your max-size is small. Dozzle shows what exists. Check the daemon default in /etc/docker/daemon.json:
| |
Without those options Docker’s default is unlimited, which is its own disaster — I have seen a single chatty container produce 60 GB of json-file and fill a root partition. Setting a cap is part of the same discipline as pruning and pinning your images, and both are the sort of housekeeping nobody does until the disk is full.
An agent shows as disconnected. Port 7007 is not reachable from the hub, or there is a version skew between hub and agent. Keep them on the same tag; agent mode has no long-term compatibility promise across majors.
Logs are interleaved oddly in split view. Container clocks. Both containers report their own timestamps and if one host’s clock has drifted the merge looks wrong. Check NTP before blaming Dozzle.
Browser tab pegs a CPU core. A container producing several thousand lines a second will do that — Dozzle renders every line in the DOM. Use the search filter, which drops non-matching lines before render, or fix the container that is screaming.
The honest verdict
The case against Dozzle is that it solves a problem you could solve with a terminal multiplexer and better discipline, and that it needs the Docker socket to do it. Both are fair. If you never run more than one container’s logs at a time, docker logs -f is right there and needs no auth story.
The case for is the four-minute question. When something is on fire, the tool you want is the one that shows you two log streams side by side, on the right host, in under ten seconds, with a search box. Dozzle does that and costs 20 MB. Nothing else in the category is close on that ratio.
Where it sits: Dozzle and Loki solve different problems and I run both. Loki answers “what happened on Tuesday” and needs a collector on every host, a storage backend and a label schema. Dozzle answers “what is happening right now” and needs nothing. When something breaks, I open Dozzle first, every single time, and I only reach for Loki when the answer is older than the log buffer. In the same way, Uptime Kuma tells me something is down and Dozzle tells me why, usually within a minute of the notification arriving.
Who should install it: anyone running more than about five containers. The setup is one compose block and the payoff arrives the first time something breaks.
Who should skip it: anyone unwilling to mount the Docker socket without a proxy in front, on a host where that risk is real, who has not got round to setting up the proxy. That is a legitimate position and the fix is an afternoon’s work rather than a reason to give up. And anyone who genuinely needs history — if the answer you want is routinely older than your log rotation, you want an aggregation stack and Dozzle will only frustrate you.




