Skip to content
Go back

Lazy Docker & Dive: CLI Tools That Make Docker Less Painful

By SumGuy 15 min read
Lazy Docker & Dive: CLI Tools That Make Docker Less Painful

Docker’s CLI Is Technically Fine (and That’s the Problem)

Let’s get something out of the way: docker ps, docker logs, docker stats — they all work. They’re functional. They get the job done in the same way that a spoon technically works as a screwdriver. Nobody’s going to stop you, but you’re making life harder than it needs to be.

The Docker CLI was designed by people who think in terms of composable Unix commands and pipe chains. And that’s great if you’re writing shell scripts. But if you’re a human being who just wants to see what’s running, check some logs, and maybe restart a container without typing four different commands, you deserve better tools.

Enter lazydocker and dive — two CLI tools that approach Docker from completely different angles but share the same core philosophy: you shouldn’t need a photographic memory of Docker’s 50+ subcommands to be productive.

Lazydocker gives you a full terminal UI for managing containers, images, volumes, and networks. Dive lets you crack open Docker images and inspect every layer to figure out why your “simple” Node app is somehow 1.2 GB. Together, they cover the two biggest pain points in daily Docker work: management and optimization.

Let’s dig in.

Lazydocker: A Full Docker Dashboard in Your Terminal

Lazydocker is from Jesse Duffield, the same person who made lazygit (which should tell you something about his feelings toward typing long commands). It’s a terminal UI that gives you a real-time dashboard of everything Docker is doing on your system.

Think of it as docker ps + docker logs + docker stats + docker compose all fused together in a single interactive interface that you navigate with your keyboard. It’s what Docker Desktop wishes it was, minus the Electron memory footprint and the questionable licensing decisions.

Installing Lazydocker

You’ve got options. Pick your poison.

Homebrew (macOS and Linux):

Terminal window
brew install lazydocker

Go install (if you have Go 1.21+):

Terminal window
go install github.com/jesseduffield/lazydocker@latest

Binary release (any Linux distro):

Terminal window
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash

Docker (because of course):

Terminal window
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.config/lazydocker:/.config/jesseduffield/lazydocker \
lazyteam/lazydocker

Yes, you can run a Docker management tool inside Docker. It’s turtles all the way down.

Nix:

Terminal window
nix-env -iA nixpkgs.lazydocker

Once it’s installed, just type lazydocker in any terminal and you’re in.

The Interface: What You’re Looking At

When lazydocker launches, you’ll see a multi-panel layout that might feel familiar if you’ve ever used tmux or any tiling window manager. Here’s the breakdown:

The whole thing updates in real time. Container CPU and memory usage are graphed live. Logs stream in as they happen. It’s like having watch docker stats and docker logs -f running simultaneously, except you don’t need six terminal tabs to do it.

Essential Keybindings

Here’s where lazydocker really shines. Instead of remembering docker restart my-container-name, you navigate to the container and press a key:

KeyAction
enterFocus on selected item
dRemove container/image/volume
sStop container
rRestart container
aAttach to container
mView logs
eOpen shell in container (exec)
EExec with custom command
bView bulk commands
cRun custom command
[ / ]Switch between panels (containers, images, volumes, networks)
xOpen menu for current item
/Filter

The x key is the big one. It opens a context menu with every action available for whatever you’ve selected. If you forget everything else, remember x.

For containers specifically, you can:

For images:

Customizing Lazydocker

The config file lives at ~/.config/lazydocker/config.yml. Here are some tweaks worth making:

gui:
scrollHeight: 2
language: 'auto'
theme:
activeBorderColor:
- green
- bold
inactiveBorderColor:
- white
selectedLineBgColor:
- blue
reporting: "off"
commandTemplates:
dockerCompose: "docker compose"
restartService: "docker compose restart {{ .Service.Name }}"
logs:
timestamps: true
since: "60m"
tail: "200"
oS:
openCommand: "xdg-open {{filename}}"

A few things worth noting:

Custom Commands: The Secret Weapon

You can define custom commands per resource type in the config:

customCommands:
containers:
- name: "View container IP"
attach: false
command: "docker inspect --format '{{ .NetworkSettings.IPAddress }}' {{ .Container.ID }}"
- name: "Export logs to file"
attach: false
command: "docker logs {{ .Container.ID }} > /tmp/{{ .Container.Name }}_logs.txt 2>&1"
- name: "Follow logs in external terminal"
attach: false
command: "xterm -e 'docker logs -f {{ .Container.ID }}'"
images:
- name: "Dive into image"
attach: true
command: "dive {{ .Image.ID }}"

That last one is chef’s kiss — it lets you select an image in lazydocker and jump straight into dive for layer analysis. Which brings us to…

Dive: X-Ray Vision for Docker Images

Dive is a tool for exploring Docker images, their layer contents, and figuring out where all that disk space went. If you’ve ever stared at a 900 MB image and wondered “but my app is only 15 lines of Python,” dive is about to become your best friend.

The core insight behind dive is simple: Docker images are made of layers, and each layer adds files. But you can’t see what’s in those layers easily. docker history gives you a vague summary. docker inspect gives you JSON that requires an advanced degree to parse. Dive gives you an actual file browser where you can walk through each layer and see exactly what was added, modified, or removed.

Installing Dive

Homebrew:

Terminal window
brew install dive

Go install:

Terminal window
go install github.com/wagoodman/dive@latest

Debian/Ubuntu (.deb):

Terminal window
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/')
curl -OL "https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb"
sudo apt install "./dive_${DIVE_VERSION}_linux_amd64.deb"

Docker:

Terminal window
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest <your-image>

Using Dive: The Basics

Point dive at any image:

Terminal window
dive nginx:latest

You can also build and analyze in one step:

Terminal window
dive build -t my-app:latest .

This runs docker build and immediately opens the result in dive. No extra steps. It’s the kind of workflow integration that makes you wonder why every Docker tool doesn’t do this.

The Dive Interface

Dive gives you a two-panel layout:

Left panel: Layers

Right panel: File tree

Key Dive Keybindings

KeyAction
TabSwitch between layers and file tree
Ctrl+AToggle showing added files
Ctrl+RToggle showing removed files
Ctrl+MToggle showing modified files
Ctrl+UToggle showing unmodified files
Ctrl+BToggle showing file attributes
Ctrl+SpaceToggle collapsing all directories
SpaceCollapse/expand directory
Ctrl+LShow layer changes only
Ctrl+AShow aggregated changes
Ctrl+FFilter files

The filter feature (Ctrl+F) is particularly useful. You can search for specific files or patterns across the entire image. Looking for that mystery .cache directory eating 400 MB? Filter for it.

Practical Dive Walkthrough: Why Is This Image So Big?

Let’s walk through a real scenario. You have this Dockerfile:

FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]

You build it, and the image is 1.3 GB. Your app is 200 lines of TypeScript. Something has gone horribly wrong.

Run dive:

Terminal window
dive build -t my-app:debug .

Here’s what you’ll likely find:

  1. Layer 1 (FROM node:20) — ~350 MB. That’s the full Node.js runtime on top of Debian. Already a chonker.
  2. Layer 3 (npm install) — ~450 MB. Your node_modules directory in all its glory. Every dependency and its cousin.
  3. Layer 4 (COPY .) — ~200 MB. Wait, that copied node_modules again? And your .git directory? And that 150 MB test fixture file you forgot about?
  4. Layer 5 (npm run build) — ~50 MB. The actual build output.

Dive makes all of this obvious at a glance. You can see the duplicate node_modules, the .git directory that has no business being in a production image, and the test data that tagged along for the ride.

The Optimized Version

Armed with dive’s insight, you rewrite:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

Add a .dockerignore:

.git
node_modules
*.md
tests/
.env
.vscode

Run dive again on the new build. Instead of 1.3 GB, you’re looking at ~180 MB. That’s the power of actually seeing what’s in your layers instead of guessing.

Dive’s Image Efficiency Score

At the bottom of the dive UI, you’ll see an efficiency score. This metric tells you how much wasted space exists in your image — files that were added in one layer and removed in another (but still take up space because that’s how layers work).

A score of 100% means no wasted space. Anything below 95% is worth investigating. Below 90%? You’ve got problems.

Common efficiency killers:

The fix is almost always to combine operations in a single RUN statement:

RUN apt-get update && \
apt-get install -y --no-install-recommends some-package && \
rm -rf /var/lib/apt/lists/*

CI Integration: Automated Image Checks with Dive

Dive isn’t just an interactive tool. You can run it in CI to automatically fail builds when images are too bloated. This is where it goes from “nice to have” to “essential.”

Basic CI Usage

Terminal window
CI=true dive <your-image> --ci-config .dive-ci.yml

When the CI environment variable is set, dive runs non-interactively and returns an exit code based on your config.

Dive CI Configuration

Create a .dive-ci.yml in your project root:

rules:
# If the efficiency is measured below X%, mark as failed.
lowestEfficiency: 0.9
# If the amount of wasted space is at least X or larger than X, mark as failed.
highestWastedBytes: 50mb
# If the amount of wasted space makes up for X% or more of the image, mark as failed.
highestUserWastedPercent: 0.15

GitHub Actions Example

name: Docker Image Check
on:
push:
paths:
- 'Dockerfile'
- '.dockerignore'
- 'package*.json'
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t my-app:ci .
- name: Install dive
run: |
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/')
curl -OL "https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb"
sudo apt install "./dive_${DIVE_VERSION}_linux_amd64.deb"
- name: Analyze image
run: CI=true dive my-app:ci --ci-config .dive-ci.yml

GitLab CI Example

image-analysis:
stage: test
image: docker:latest
services:
- docker:dind
variables:
DOCKER_TLS_CERTDIR: ""
script:
- docker build -t my-app:ci .
- |
apk add --no-cache curl
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/')
curl -OL "https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.tar.gz"
tar -xzf dive_${DIVE_VERSION}_linux_amd64.tar.gz
mv dive /usr/local/bin/
- CI=true dive my-app:ci --ci-config .dive-ci.yml

Now every PR that touches the Dockerfile or dependencies gets automatically checked for image bloat. No more “oh we’ll optimize it later” that never actually happens.

Bonus Tool: DockerSlim (now just Slim)

If lazydocker is your dashboard and dive is your x-ray machine, then Slim (formerly DockerSlim) is the surgeon. It automatically analyzes and optimizes your Docker images by removing everything that isn’t needed at runtime.

How Slim Works

Slim takes a different approach than manual optimization. Instead of you figuring out what to remove, it:

  1. Launches your container in a sandboxed environment
  2. Monitors which files are actually accessed at runtime using sensors and probes
  3. Builds a new minimal image containing only those files

The results can be dramatic. A 300 MB image might slim down to 30 MB because it turns out 90% of the files in there were never touched.

Installing Slim

Terminal window
curl -sL https://raw.githubusercontent.com/slimtoolkit/slim/master/scripts/install-slim.sh | sudo -E bash -

Or with Homebrew:

Terminal window
brew install slim

Basic Usage

Terminal window
slim build --target my-app:latest --tag my-app:slim

That’s it. Slim will analyze the image, run probes, and output a minified version tagged as my-app:slim.

Slim with HTTP Probing

For web applications, Slim can probe HTTP endpoints to ensure more code paths are exercised:

Terminal window
slim build \
--target my-app:latest \
--tag my-app:slim \
--http-probe-cmd /health \
--http-probe-cmd /api/status \
--expose 3000

The Slim + Dive Workflow

Here’s the real power move: use all three tools together.

  1. Build your image normally
  2. Run dive to understand the layer structure and spot obvious waste
  3. Optimize your Dockerfile based on dive’s findings
  4. Run slim to further minify the result
  5. Run dive again on the slim output to verify
Terminal window
# Step 1: Build
docker build -t my-app:latest .
# Step 2: Analyze
dive my-app:latest
# Step 3: (manually optimize Dockerfile based on findings)
# Step 4: Slim it down
slim build --target my-app:latest --tag my-app:slim
# Step 5: Verify
dive my-app:slim
# Step 6: Compare
docker images | grep my-app

A Word of Caution with Slim

Slim is powerful but not magic. It can break things if your application accesses files dynamically or at paths that weren’t exercised during probing. Common gotchas:

Always test your slimmed images thoroughly. Run your full test suite against them. If something breaks, you can use slim’s --include-path flag to explicitly keep specific files or directories.

The Complete Docker Toolbelt

Here’s how these tools fit together in a practical workflow:

TaskToolWhy
Monitor running containerslazydockerReal-time TUI dashboard
Debug container issueslazydockerQuick log access, shell exec
Analyze image sizediveLayer-by-layer file browser
CI image quality gatesdive (CI mode)Automated efficiency checks
Aggressive image optimizationslimAuto-minification
Day-to-day Docker managementlazydockerFaster than raw CLI

None of these tools replace understanding Docker fundamentals. You still need to know how layers work, why multi-stage builds matter, and what a .dockerignore does. But they make the gap between “knowing the concepts” and “actually applying them efficiently” a lot smaller.

Quick Start: Get Running in 5 Minutes

If you want to go from zero to productive as fast as possible, here’s your speed run:

Terminal window
# Install both tools
brew install lazydocker dive
# Start your Docker environment
docker compose up -d
# Open lazydocker to see everything
lazydocker
# In another terminal, analyze an image
dive nginx:latest

Spend 10 minutes clicking around lazydocker. Navigate between containers, check logs, look at stats. Then switch to dive and explore a few images. Once you’ve used both tools for a day, the raw Docker CLI will feel like going back to dial-up internet.

Final Thoughts

Docker is one of those technologies where the learning curve isn’t really about understanding the concepts — it’s about managing the complexity that grows as you add more containers, more images, and more services. The raw CLI scales poorly for humans. Dashboards and visual tools aren’t cheating; they’re how you stay sane.

Lazydocker turns Docker management from a memory test into a visual, interactive experience. Dive turns image optimization from guesswork into science. And slim takes it further by automating the tedious parts.

Install them. Use them. Your future self — the one who won’t have to type docker logs --tail 100 -f that-container-with-the-really-long-name ever again — will thank you.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Woodpecker CI vs Drone CI: Lightweight Pipelines for People Who Hate Waiting
Next Post
The MTU Problem Nobody Diagnoses Correctly

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts