> ## Content Index
> Fetch the complete content index at: https://corti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Monitoring Multiple Docker Hosts with Dozzle — the Easy Way
- URL: https://corti.com/monitoring-multiple-docker-hosts-with-dozzle-the-easy-way/
- Published: 2026-07-23T07:17:09.000Z
- Updated: 2026-07-23T07:17:09.000Z
- Author: Sascha Corti

If you run Docker on more than one machine, you know the drill: something misbehaves, and you start SSH-hopping between hosts, running `docker ps` and `docker logs -f` until you find the culprit. It works, but it doesn't scale — and it's miserable at 2 a.m.

[Dozzle](https://dozzle.dev/?ref=corti.com) fixes this. It's a lightweight, real-time log viewer for Docker that runs as a single container, needs no database, and — the killer feature — can aggregate logs from **multiple hosts** through its agent mode. One browser tab, all your machines.

In my homelab, that's four hosts: two servers (`quasar` and `darkstar`) and two DGX Spark nodes (`pulsar` and `magentar`). Then, there's another four VMs I'm running in the cloud.

![](https://corti.com/content/images/2026/07/dozzle-main-1.png)

Dozzle's main overview page

I can access a server with a click, see all its containers, and watch their logs in real time.

![](https://corti.com/content/images/2026/07/dozzle-host.png)

A container's logs on one of the hosts

If needed, I can open a shell in the container.

![](https://corti.com/content/images/2026/07/dozzle-shell.png)

Dozzle opening a shell in the container

There is a menu that lets me filter logs, restart or update containers etc.

![](https://corti.com/content/images/2026/07/dozzle-menu.png)

Dozzle's menu

Here's the full setup, including two gotchas that cost me some debugging time.

## Architecture

Dozzle has two roles, both served by the same image:

- **Agents** run on every host you want to monitor. They talk to the local Docker socket and expose it over a TLS-secured connection on port 7007.
- **One main instance** runs the web UI and connects to all agents.

Agents are the recommended approach over the older remote-host/socket-proxy setups: you never expose the raw Docker socket over the network, and all agent communication is TLS-encrypted out of the box.

## Step 1: Deploy an agent on every host

On each machine you want to monitor, create a `compose.yml`:

```yaml
services:
  dozzle-agent:
    image: amir20/dozzle:latest
    restart: unless-stopped
    command: agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      - 7007:7007

```

Then:

```bash
docker compose up -d

```

Two details worth noting:

- `restart: unless-stopped` makes the agent survive host reboots (as long as the Docker daemon itself is enabled at boot — check with `systemctl is-enabled docker`). Unlike `restart: always`, it respects a manual `docker compose stop` across reboots, which is what you usually want for maintenance.
- The socket is mounted **read-only** (`:ro`). The agent only needs to read logs and container metadata.

## Step 2: Deploy the main instance

On your "hub" host — `quasar` in my case — the compose file looks like this:

```yaml
services:
  dozzle:
    image: amir20/dozzle:latest
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./data:/data
    ports:
      - 8080:8080
    environment:
      # Enable container actions (stop, start, restart)
      - DOZZLE_ENABLE_ACTIONS=true
      # Allow access to container shells
      - DOZZLE_ENABLE_SHELL=true
      # File-based authentication (users.yml in ./data)
      - DOZZLE_AUTH_PROVIDER=simple
      # Label for this local host in the UI
      - DOZZLE_HOSTNAME=quasar
      # Remote agents: endpoint|name|group
      - DOZZLE_REMOTE_AGENT=192.168.0.1:7007|Darkstar|Servers,192.168.0.2:7007|Pulsar|Sparks,192.168.0.3:7007|Magentar|Sparks,external1.server.com:7007|External1|VMs,external2.server.com:7007|External2|VMs

```

The interesting bits:

`**DOZZLE_REMOTE_AGENT**` takes a comma-separated list of agents. Each entry uses the format `endpoint|name|group`, where name and group are optional. The name overrides what's shown in the UI; the group turns the sidebar into collapsible sections — mine are `Servers` , `Sparks` and `VMs`, each with a "merge all" button that streams the combined logs of every host in the group. If you manage more than a couple of machines, groups alone are worth the setup.

**`DOZZLE_ENABLE_ACTIONS` and `DOZZLE_ENABLE_SHELL`** let you start/stop/restart containers and open a shell in them straight from the browser. Powerful — which is exactly why you want authentication in front of it.

**Bind mount instead of a named volume for `/data`.** Dozzle's simple auth provider reads `/data/users.yml`. With a named volume you'd have to reach into `/var/lib/docker/volumes/...` or `docker cp` the file into the container. A bind mount (`./data:/data`) keeps `users.yml` next to your compose file, versionable and editable.

## Step 3: Create users.yml

Don't hand-craft the bcrypt hash — Dozzle's image ships a generator:

```bash
mkdir -p data
docker run -it --rm amir20/dozzle generate admin \
  --password 'YOUR_PASSWORD' \
  --email admin@example.com \
  --name "Admin" > data/users.yml

```

The result looks like:

```yaml
users:
  admin:
    email: admin@example.com
    name: Admin
    password: $2a$11$...   # bcrypt hash

```

Start it up:

```bash
docker compose up -d

```

Browse to `http://your.dozzle.com:8080`, log in, and you'll see all hosts in the sidebar, grouped and labeled.

## Gotcha 1: Duplicate host IDs on cloned machines

After connecting all agents, one of my Spark nodes refused to show up, and the main instance logged:

```
{"level":"warn","name":"Magentar","message":"An agent with an existing ID was
found. Removing the duplicate host. ..."}

```

Dozzle identifies each host by Docker's system ID (the node ID in Swarm mode). That ID lives in `/var/lib/docker/engine-id`and is generated when the Docker daemon first starts. If machines were provisioned from the same image or restored from the same backup — very common with identically set up nodes like my DGX Sparks — they end up with **the same engine ID**, and Dozzle silently drops the "duplicate."

Verify on both hosts:

```bash
docker system info --format '{{ .ID }}'

```

If two hosts print the same ID, fix it on one of them:

```bash
sudo rm /var/lib/docker/engine-id
sudo systemctl restart docker        # regenerates a fresh UUID
docker compose restart dozzle-agent  # agent caches the ID at startup

```

Heads-up: restarting the Docker daemon restarts every container on that host, so pick a convenient moment. After that, both nodes appeared as distinct hosts.

## Gotcha 2: Putting the *local* host into a group

The `endpoint|name|group` syntax only applies to remote agents. The localhost connection is special: `DOZZLE_HOSTNAME`can rename it, but there's no documented way to assign it to a group — so it sits ungrouped in the sidebar while everything else is neatly sorted.

The clean workaround: treat the hub like any other host. Run an agent *next to* the main instance in the same compose project, drop the socket mount from the main service, and connect to the local agent through the group syntax:

```yaml
services:
  dozzle:
    image: amir20/dozzle:latest
    restart: unless-stopped
    volumes:
      - ./data:/data          # docker.sock no longer needed here
    ports:
      - 8080:8080
    environment:
      - DOZZLE_ENABLE_ACTIONS=true
      - DOZZLE_ENABLE_SHELL=true
      - DOZZLE_AUTH_PROVIDER=simple
      - DOZZLE_REMOTE_AGENT=localhost:7007|Quasar|Servers,192.168.0.1:7007|Darkstar|Servers,192.168.0.2:7007|Pulsar|Sparks,192.168.0.3:7007|Magentar|Sparks,external1.server.com:7007|External1|VMs,external2.server.com:7007|External2|VMs

  dozzle-agent:
    image: amir20/dozzle:latest
    restart: unless-stopped
    command: agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

```

Three things make this work:

1. Both services share the compose project's default network, so the main instance reaches the agent at `dozzle-agent:7007` — no `ports:` mapping needed on the agent.
2. Without the socket mounted on the main instance, the UI shows *only* agent-connected hosts — no duplicate "localhost" entry.
3. The `|Quasar|Servers` label handles naming, making `DOZZLE_HOSTNAME` redundant.

Now every host — including the hub itself — lives in a group.

## Wrap-up

The full setup is two small compose files and one generated `users.yml`:

- an agent per host, socket mounted read-only, `restart: unless-stopped`
- one main instance with simple auth, actions and shell enabled, and grouped agents via `endpoint|name|group`

Total effort: maybe an hour, most of which I spent on the engine-id issue you now get to skip. In return you get real-time logs, container actions, and shell access for your whole fleet in a single browser tab — with zero databases, zero log shippers, and zero SSH-hopping.