Skip to content
SumGuy's Ramblings
Go back

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):

brew install lazydocker

Go install (if you have Go 1.21+):

go install github.com/jesseduffield/lazydocker@latest

Binary release (any Linux distro):

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

Docker (because of course):

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:

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:

brew install dive

Go install:

go install github.com/wagoodman/dive@latest

Debian/Ubuntu (.deb):

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:

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:

dive nginx:latest

You can also build and analyze in one step:

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:

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

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

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

Or with Homebrew:

brew install slim

Basic Usage

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:

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
# 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:

# 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:

Previous Post
Woodpecker CI vs Drone CI: Lightweight Pipelines for People Who Hate Waiting
Next Post
Gitea vs Forgejo vs GitLab CE: Self-Hosted Git Without the Existential Crisis