Written against Dozzle v11.1.x, September 2026. v11 shipped on September 11, so expect details to keep moving.
docker logs -f is fine for one container. Then you end up with a Compose stack of eight services, or three hosts, and a bug that only shows up when the API, the worker, and the database are all unhappy at the same moment. Now you're flipping between terminal tabs, trying to line up timestamps by eye.
Dozzle solves that one problem. It's a small web app that shows container logs live in your browser: open the page, click a container, watch the lines arrive. This post goes from the two-minute install through search, alerts, multiple hosts, Kubernetes, and locking it down. It's based on v11.
What it is, and what it isn't
Dozzle is a live tail, nothing more. It doesn't store logs. It reads from the Docker API, the same place docker logs reads from, so what you see is whatever Docker still holds, and how much that is depends on your logging driver's rotation settings. Once Docker drops a line, Dozzle can't show it.
Keep in mind: Dozzle is a live viewer, not a log store. If you need history, it has to come from Docker's log settings or a separate logging stack.
The upside of being that simple is that the image is only a few megabytes compressed and there's next to nothing to configure before logs appear. It works with Docker, Swarm, and Kubernetes, and with Colima and Podman too. Podman needs its remote socket enabled first.
The limits are worth knowing up front. The project says it's been tested with hundreds of containers, but it has no offline searching, and it points people who need full search toward tools like Loggly, Papertrail, or Kibana. Dozzle is for watching what's happening right now, not for digging through last week.
What changed in v10 and v11
A few things worth knowing if you last used Dozzle a while ago:
v10 introduced alerts with webhook delivery. Today they cover logs, resource metrics, and container events.
v11 is the biggest visual overhaul so far: flat, neutral panels, with color saved for things that need your attention. It also brought GitHub and OIDC sign-in, recognition of more log formats, and alerts that persist across reloads.
v11.1 added a separate
oidcauth provider that reads users and roles from the token, a login-first setup wizard for fresh installs, andgenerate-certsfor giving agents their own certificate.
One upgrade catch: session tokens are now signed with a random secret kept in the data directory, so everyone gets signed out once after upgrading.
Quick start
The one-liner:
docker run -d --name dozzle \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v dozzle_data:/data \
-p 8080:8080 \
amir20/dozzle:latest
Open http://localhost:8080 and your containers should be listed. For something you plan to keep running, a Compose file is easier to maintain:
services:
dozzle:
image: amir20/dozzle:latest # pin a specific version tag in production
container_name: dozzle
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./dozzle-data:/data
environment:
DOZZLE_NO_ANALYTICS: "true"
Some notes on that file:
Mount
/data. Alert and destination settings are stored there, so without a volume they vanish on restart. User settings and yourusers.ymllive there too.Dozzle sends anonymous usage analytics by default.
DOZZLE_NO_ANALYTICSturns that off.Pin the image tag. With Dozzle moving fast (v11 signed everyone out on upgrade),
latestcan bite you at a bad time.
Two habits worth having from day one: mount
/dataso your settings survive restarts, and pin the image tag instead of usinglatest.
Getting around the interface
The sidebar lists your containers and groups Compose services by stack name automatically. v11 rebuilt it around collapsible groups with counts, and each container's icon carries a status badge. Container names are fuzzy-searchable, so on a busy host you type a few letters and jump straight to the service.
Logs stream in the main pane. Dozzle detects JSON logs and pretty-prints them, and if your entries have a level field they're colored by severity. In v11, warn and error rows get a light tint so they stand out as you scroll, and a live indicator plus a floating scroll readout show where you are in the container's lifetime. If you only care about problems, one click hides the info and debug lines.
Split view is the feature that actually replaces terminal tabs. It puts several containers side by side, so when the API returns a 500 you can watch the database and cache logs at the same timestamp. In v11 the pinned columns are stored in the URL, which means a side-by-side view is just a link you can send to a teammate.
Each container also gets small CPU and memory charts. They're basic, but enough to tell whether a container is struggling.
Searching and querying logs
For quick filtering there's regex search over the logs. For anything more analytical there's a SQL engine.
The SQL engine runs DuckDB compiled to WebAssembly inside your browser, so your logs never leave your machine. Dozzle loads your JSON logs into a virtual logs table that you can query. You open it from the menu or with Ctrl/Cmd+Shift+F, and it only works on JSON-structured logs. The docs still label it beta.
It queries what's already loaded in the browser, not Docker's full history. That makes it good for ad-hoc debugging, but don't expect trend analysis from it. WebAssembly caps it at 4 GB of memory, and if you run out you refresh the page.
-- How noisy is each severity right now?
SELECT level, COUNT(*) AS n
FROM logs
GROUP BY level;
-- Slowest failing requests (field names depend on your JSON logs)
SELECT message.path, message.status, message.duration
FROM logs
WHERE message.status >= 500
ORDER BY message.duration DESC
LIMIT 20;
-- Errors per minute
SELECT date_trunc('minute', timestamp) AS minute, COUNT(*) AS error_count
FROM logs
WHERE level = 'error'
GROUP BY minute
ORDER BY minute DESC;
If you already emit structured logs, this can replace a lot of docker logs | jq | grep pipelines.
Grouping and naming containers
Dozzle groups by stack by default. To make your own groups, add the dev.dozzle.group label, and containers that share a group name end up together in the UI. There's also a dev.dozzle.name label if you want a friendlier display name.
services:
api:
image: myorg/api:1.4.2
labels:
dev.dozzle.group: shop
dev.dozzle.name: shop-api
Under Swarm, if Dozzle sees the service-name label, it switches to a swarm view that joins all tasks of the same service.
Limiting what Dozzle can see
DOZZLE_FILTER restricts which containers Dozzle can see at all. Filters are passed straight to Docker, in the same style as docker ps --filter, so DOZZLE_FILTER=label=color shows only containers that carry that label. They can also be set per agent and per user, and they stack: a container has to match all of them to show up.
Be careful with filters that exclude stopped containers, like status=running. The container that just crashed is often the one you need to read, and a filter like that hides it completely.
Security
Mounting the Docker socket gives a container effectively root-level access to the host, and the :ro in the examples above doesn't change that. It only marks the socket file read-only on disk, so API calls still pass through and create, delete, and update operations stay possible. If you don't need actions, put a socket proxy such as tecnativa/docker-socket-proxy between Dozzle and the daemon to limit what it can do.
An unauthenticated Dozzle on a reachable network also shows every container's logs to anyone who finds it, and logs often contain tokens and personal data.
Rule of thumb: no authentication, no exposure beyond localhost.
Built-in auth
Start by generating a users file:
docker run -it --rm amir20/dozzle generate admin \
--password 'change-me' \
--email admin@example.com \
--name "Admin" > users.yml
Put users.yml in your mounted /data directory and set DOZZLE_AUTH_PROVIDER: simple. Passwords are stored bcrypt-hashed. Each user can also have a filter, which restricts which containers they can see by label, and roles, which control what they can do: shell, actions, download, notifications, and cloud. A user with no roles listed gets all of them, so set roles explicitly for anyone who shouldn't have full access. The instance-wide flags for shell and actions still have to be on before those roles do anything.
GitHub and OIDC (v11)
v11 lets you sign in with GitHub or any OIDC provider, such as Authentik, Keycloak, Pocket ID, or Google. It sits on top of the simple provider, so users.yml stays the allowlist, no accounts are created automatically, and password login keeps working. If you'd rather manage users and roles in your identity provider, v11.1 added a separate oidc provider that reads them from the token.
environment:
DOZZLE_AUTH_PROVIDER: simple
DOZZLE_AUTH_GITHUB_CLIENT_ID: <your-client-id>
DOZZLE_AUTH_GITHUB_CLIENT_SECRET: <your-client-secret>
Forward-proxy auth
In production, Dozzle can trust identity headers from a proxy like Authelia, Authentik, or Cloudflare Access. That's the better route if you want centralized multi-factor auth, but it comes with one hard rule: Dozzle believes the Remote-User header on every request. Publish only the proxy and keep Dozzle on an internal network (expose, not ports), because anyone who can reach Dozzle directly can set that header and log in as whoever they like. Also map roles from your proxy, for example DOZZLE_AUTH_HEADER_ROLES: Remote-Groups for Authelia groups, since without a mapping every authenticated user gets all roles.
Actions and shell are opt-in
Container start/stop/restart actions (DOZZLE_ENABLE_ACTIONS) and shell access (DOZZLE_ENABLE_SHELL) are off by default. If you turn either on, get authentication in place first. They give the web UI the same power as docker stop and docker exec.
Reverse proxy
Dozzle streams logs over Server-Sent Events and uses WebSockets for shell and attach. That gives a reverse proxy three jobs: don't buffer responses, forward the WebSocket upgrade headers, and don't compress text/event-stream. Buffering makes logs arrive in bursts or not at all. A minimal nginx location:
location / {
proxy_pass http://127.0.0.1:8080;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
The long read timeout matters too, because logs stop after a few seconds when the proxy's timeouts are short. Behind Traefik, the default compress middleware breaks SSE, so exclude text/event-stream. In Caddy, flush_interval -1 turns off response buffering. And if you mount Dozzle under a sub-path with DOZZLE_BASE, make sure the proxy passes the full path through instead of stripping the prefix.
Proxy tip: if logs arrive in bursts or not at all, response buffering is the first thing to turn off.
Keep it updated
Dozzle's security page lists several advisories from 2026, including these high-severity ones:
an unauthenticated SSRF through the webhook test endpoint on default deployments without auth
cross-site WebSocket hijacking on the exec and attach endpoints, which got around authentication for setups with shell enabled (versions up to 10.5.1)
a label-based access bypass in the agent that allowed unauthorized shell access
So: turn on auth, keep the container patched, and keep it off the open internet.
Monitoring multiple hosts with agents
To see several machines in one UI, run Dozzle in agent mode on each remote host and point a central instance (the hub) at them. Agents listen on port 7007, and the hub connects to them over TLS.
# On each remote host
services:
dozzle-agent:
image: amir20/dozzle:latest
command: agent
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- "7007:7007" # keep this on a private network
# On the central host
services:
dozzle:
image: amir20/dozzle:latest
volumes:
- ./data:/data
ports:
- "8080:8080"
environment:
DOZZLE_AUTH_PROVIDER: simple # expects users.yml in ./data
DOZZLE_REMOTE_AGENT: "10.0.1.10:7007|web-1|production,10.0.1.11:7007|web-2|production"
The connection string looks like endpoint|name|group. All three parts are optional, and groups show up as collapsible sections in the sidebar, each with a button that merges the group's logs into one view. If the hub only needs to show remote hosts, you can skip mounting the local socket there. If you run Swarm, you don't need agents at all, because Dozzle discovers the cluster on its own.
Treat the agent port as sensitive. The TLS certificate Dozzle ships with is identical in every copy of the image, so it encrypts the connection but doesn't prove who is on the other end. Anyone who can reach port 7007 can connect their own Dozzle to your agent, read every log on that host, and run commands inside its containers. The agent also ignores DOZZLE_ENABLE_SHELL and DOZZLE_ENABLE_ACTIONS, because those flags only control what the UI offers. Keep 7007 on a private network (on a shared Docker network you don't need to publish it at all), and if anything you don't control can reach it, generate your own certificate with generate-certs so agents only accept your hub.
Important: anyone who can reach port 7007 can read every log and run commands inside that host's containers. Keep it on a private network.
Alerts
Since v10, Dozzle can tell you when something breaks instead of waiting for you to notice. It watches logs, resource metrics, and lifecycle events, evaluates your rules on your own instance, and sends notifications to a webhook, Slack, Discord, or ntfy.
Each alert has a container expression, which decides which containers to watch, and a trigger expression. Triggers come in three types: log, metric, and event. Setup lives on the Notifications page: add a destination first, then create rules. Webhook destinations come with built-in Slack, Discord, and ntfy payloads, and you can write custom Go text/template payloads for anything else. There's a Test button, so you can confirm delivery before saving.
Some example rules, written in the expression style the docs use:
# 5xx responses from production APIs
Container: name contains "api" && labels["env"] == "production"
Log: message.status >= 500
# Memory pressure on the database
Container: name == "postgres"
Metric: memory > 85
# Any OOM kill, anywhere
Container: true
Event: name == "oom"
Metric alerts evaluate a smoothed average over a sample window and have a cooldown between triggers, so a brief spike doesn't flood your channel. For die events, the docs' example excludes exit codes 0, 130, 143, and 137, since those show up on routine stops and update cycles.
Dozzle Cloud is optional. Your rules always live on your self-hosted instance, but if you link it, delivery features such as grouping repeated failures, summaries, muting, and mobile channels are configured there.
Alerts are deliberately simple. There are no escalation policies or on-call rotations, so treat them as a safety net for staging and homelabs, not as a production pager.
Kubernetes
For Kubernetes, run Dozzle with DOZZLE_MODE=k8s. The docs include a full RBAC manifest; at minimum it needs read access to pods, pod logs, and nodes. Logs work without the Kubernetes Metrics API (metrics-server), but CPU and memory stay empty without it. Give it a persistent volume for /data so your alert config survives restarts.
env:
- name: DOZZLE_MODE
value: "k8s"
- name: DOZZLE_NAMESPACE
value: "prod,staging" # optional; defaults to all namespaces
- name: DOZZLE_FILTER
value: "env=prod" # optional label filter
The docs still call Kubernetes support a newer feature that may have limitations compared to the Docker version, and the release notes bear that out. v11.1.1 alone includes Kubernetes hardening, alerts for CronJob pods, and fixes for duplicate ReplicaSets and finished Jobs. If you run Dozzle on Kubernetes, keep it up to date.
Letting AI assistants read your logs (MCP)
Dozzle can expose an MCP endpoint so coding assistants can inspect your containers. It's disabled by default. Enable it with DOZZLE_ENABLE_MCP=true and it's served at /api/mcp from the same container. Every tool is read-only: listing containers and hosts, fetching and searching logs, and pulling CPU and memory history.
One warning: with no auth provider configured, the endpoint is publicly accessible, so set up authentication first. Once auth is on, MCP clients have to present credentials too.
When your app logs to files instead of stdout
Dozzle only sees what Docker captures, which means stdout and stderr, exactly like docker logs. Files inside a container are invisible to it.
The best fix is to log to the console, or symlink the log file to /dev/stdout, as the official nginx image does. If you can't, the docs suggest a small sidecar that tails the file:
docker run -d --name app-log --network none \
--label dev.dozzle.name=app-log \
--log-opt max-size=10m --log-opt max-file=3 \
-v /var/log/myapp:/logs:ro \
alpine tail -n 1000 -F /logs/app.log
Use -F instead of -f so the tail reopens the path after log rotation. Mount the directory, not the single file, because a single-file bind mount stays attached to the old inode.
Troubleshooting
Empty stream for a container that's clearly running: if it uses a remote logging driver such as splunk, fluentd, or awslogs, check whether
cache-disabledis set to true (and look atdaemon.jsontoo). That setting blocks the local cache Dozzle reads from.Logs arrive in bursts, or stop after a few seconds, behind a proxy: response buffering is on,
text/event-streamis being compressed, or the read timeout is too short. See the reverse proxy section.Shell disconnects immediately: the proxy isn't forwarding the WebSocket upgrade headers.
Won't start after following an old tutorial:
DOZZLE_USERNAMEandDOZZLE_PASSWORDare no longer supported. Useusers.ymlinstead.Alerts vanish after a restart:
/dataisn't mounted as a volume.Signed out on every restart: if
/dataisn't writable, Dozzle falls back to an in-memory session secret (and warns about it), so sessions drop whenever it restarts.Everyone logged out after upgrading to v11: expected, and it only happens once.
When to outgrow Dozzle
Dozzle answers "what is this container saying right now?" It can't answer which deploy introduced this spike, did the error rate stay high overnight, or what happened to this request across three services last week. Those need retention, correlation, and analysis over time, which a real-time viewer doesn't give you. When you reach that point, add a proper logging or observability stack, like Loki, an OpenTelemetry pipeline, or a hosted platform, and keep Dozzle for the quick look.
Checklist before you rely on it
Pin the image version and update on a schedule.
Mount
/dataas a persistent volume.Turn on authentication (
users.yml, OIDC/GitHub, or a forward proxy) before exposing it beyond localhost.Leave actions and shell off unless you need them, and put a socket proxy in front of the Docker socket if you don't need actions.
For multiple hosts, use agents instead of exposing a Docker socket, keep port 7007 on a private network, and generate your own agent certificate if it's reachable from anywhere else.
Set log rotation (
max-size,max-file) so there's enough history to look at.Log to stdout, in JSON if you can, so you get level coloring, SQL queries, and structured alerts.
References
Alerts, Agent Mode, and Kubernetes
Reverse Proxy & Base Path, Simple authentication, and Filters
Authentication, Forward Proxy, and Docker's dual logging docs
Published via ZyVOP — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium & Hashnode in 1 click.
Top comments (0)