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:
- Left sidebar — lists your containers, images, volumes, and networks. Navigate between categories with the left bracket
[and right bracket]keys, or just click if your terminal supports it. - Main panel (top right) — shows details about whatever you’ve selected. For containers, this defaults to logs.
- Main panel (bottom right) — stats, environment variables, config details, or whatever secondary info is relevant.
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:
| Key | Action |
|---|---|
enter | Focus on selected item |
d | Remove container/image/volume |
s | Stop container |
r | Restart container |
a | Attach to container |
m | View logs |
e | Open shell in container (exec) |
E | Exec with custom command |
b | View bulk commands |
c | Run custom command |
[ / ] | Switch between panels (containers, images, volumes, networks) |
x | Open 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:
- View logs with timestamps
- Restart, stop, start, pause, or remove
- Attach a shell session
- Inspect the full container config
- See real-time resource usage
For images:
- Remove individual images or dangling images
- See which containers are using an image
- View image layer history
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:
- The
logs.sincesetting controls how far back logs go when you first open them. Default is blank (all logs), which can be painfully slow for chatty containers. commandTemplates.dockerCompose— set this todocker compose(with a space) for newer Docker Compose V2, ordocker-composefor the legacy version.- Custom commands are powerful. You can define your own shortcuts that run arbitrary shell commands with template variables for the selected container, service, or image.
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
- Each layer corresponds to a Dockerfile instruction
- Shows the command that created the layer, its size, and a running total
- Layers are ordered top to bottom, oldest to newest
Right panel: File tree
- Shows the filesystem at the selected layer
- Files are color-coded:
- Green — added in this layer
- Yellow — modified in this layer
- Red — removed in this layer (but still taking space in the image!)
- White — unchanged, inherited from previous layers
Key Dive Keybindings
| Key | Action |
|---|---|
Tab | Switch between layers and file tree |
Ctrl+A | Toggle showing added files |
Ctrl+R | Toggle showing removed files |
Ctrl+M | Toggle showing modified files |
Ctrl+U | Toggle showing unmodified files |
Ctrl+B | Toggle showing file attributes |
Ctrl+Space | Toggle collapsing all directories |
Space | Collapse/expand directory |
Ctrl+L | Show layer changes only |
Ctrl+A | Show aggregated changes |
Ctrl+F | Filter 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:
- Layer 1 (FROM node:20) — ~350 MB. That’s the full Node.js runtime on top of Debian. Already a chonker.
- Layer 3 (npm install) — ~450 MB. Your
node_modulesdirectory in all its glory. Every dependency and its cousin. - Layer 4 (COPY .) — ~200 MB. Wait, that copied
node_modulesagain? And your.gitdirectory? And that 150 MB test fixture file you forgot about? - 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:
- Running
apt-get installandapt-get cleanin separateRUNcommands (the cleanup doesn’t actually save space because it’s a new layer) - Copying files in one layer and deleting them in another
- Running
npm installwith dev dependencies, then removing them later
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:
- Launches your container in a sandboxed environment
- Monitors which files are actually accessed at runtime using sensors and probes
- 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.
- Build your image normally
- Run dive to understand the layer structure and spot obvious waste
- Optimize your Dockerfile based on dive’s findings
- Run slim to further minify the result
- 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:
- Timezone data — If your app uses timezone conversion but the probe didn’t trigger it, those files might get stripped
- SSL certificates — Sometimes the CA bundle gets removed, breaking HTTPS calls
- Locale files — Internationalization can break if locale data is stripped
- Dynamically loaded libraries — If a
.sofile is loaded conditionally, slim might miss it
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:
| Task | Tool | Why |
|---|---|---|
| Monitor running containers | lazydocker | Real-time TUI dashboard |
| Debug container issues | lazydocker | Quick log access, shell exec |
| Analyze image size | dive | Layer-by-layer file browser |
| CI image quality gates | dive (CI mode) | Automated efficiency checks |
| Aggressive image optimization | slim | Auto-minification |
| Day-to-day Docker management | lazydocker | Faster 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.