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 lazydockerGo install (if you have Go 1.21+):
go install github.com/jesseduffield/lazydocker@latestBinary release (any Linux distro):
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bashDocker (because of course):
docker run --rm -it \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/.config/lazydocker:/.config/jesseduffield/lazydocker \ lazyteam/lazydockerYes, you can run a Docker management tool inside Docker. It’s turtles all the way down.
Nix:
nix-env -iA nixpkgs.lazydockerOnce 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 |
x | Open context menu |
/ | 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/remove, attach a shell session, inspect the full container config, and see real-time resource usage.
For images: remove individual 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"A few things worth noting:
logs.sincecontrols 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 Compose V2, ordocker-composefor the legacy version.
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" images: - name: "Dive into image" attach: true command: "dive {{ .Image.ID }}"That last one — select an image in lazydocker and jump straight into dive for layer analysis. Chef’s kiss.
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.
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 diveGo install:
go install github.com/wagoodman/dive@latestDebian/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:latestBuild 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.
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
Right panel: File tree
- Color-coded file changes per layer:
- 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+F | Filter files |
Space | Collapse/expand directory |
The filter feature (Ctrl+F) is particularly useful. 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:20WORKDIR /appCOPY package*.json ./RUN npm installCOPY . .RUN npm run buildEXPOSE 3000CMD ["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 build -t my-app:debug . and here’s what you’ll likely find:
- Layer 1 (FROM node:20) — ~350 MB. That’s the full Node.js runtime on Debian. Already a chonker.
- Layer 3 (npm install) — ~450 MB. Your
node_modulesdirectory in all its glory. - Layer 4 (COPY .) — ~200 MB. Wait, that copied
node_modulesagain? And your.gitdirectory? And that 150 MB test fixture you forgot about? - Layer 5 (npm run build) — ~50 MB. The actual build output.
Dive makes all of this obvious at a glance. Armed with that insight, you rewrite:
FROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ci --only=productionCOPY . .RUN npm run build
FROM node:20-alpineWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./EXPOSE 3000CMD ["node", "dist/index.js"]Add a .dockerignore:
.gitnode_modules*.mdtests/.env.vscodeRun dive again on the new build. Instead of 1.3 GB, you’re looking at ~180 MB.
Dive’s Image Efficiency Score
At the bottom of the dive UI, you’ll see an efficiency score — how much wasted space exists in your image (files added in one layer and removed in another, but still taking up space because layers).
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 install and apt-get clean in separate RUN commands (cleanup in a new layer doesn’t save space), copying files then deleting them in another layer, npm install with dev dependencies then removing them later.
The fix is almost always to combine operations in a single RUN:
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. Run it in CI to automatically fail builds when images are too bloated.
Dive CI Configuration
Create .dive-ci.yml in your project root:
rules: lowestEfficiency: 0.9 highestWastedBytes: 50mb highestUserWastedPercent: 0.15GitHub Actions Example
name: Docker Image Checkon: 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.ymlNow every PR that touches the Dockerfile gets automatically checked for image bloat. No more “we’ll optimize it later” that never actually happens.
Bonus: DockerSlim (now just Slim)
If lazydocker is your dashboard and dive is your x-ray machine, Slim (formerly DockerSlim) is the surgeon. It automatically analyzes and optimizes your Docker images by removing everything that isn’t needed at runtime.
It launches your container in a sandboxed environment, monitors which files are actually accessed, then builds a minimal image containing only those files. A 300 MB image might slim down to 30 MB.
# Installcurl -sL https://raw.githubusercontent.com/slimtoolkit/slim/master/scripts/install-slim.sh | sudo -E bash -
# Basic usageslim build --target my-app:latest --tag my-app:slim
# With HTTP probing for web appsslim build \ --target my-app:latest \ --tag my-app:slim \ --http-probe-cmd /health \ --expose 3000A word of caution: slim can break things if your app accesses files dynamically or at paths not exercised during probing. Timezone data, SSL certificates, and locale files are common casualties. Always test your slimmed images thoroughly.
The Complete Docker Toolbelt
| 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 |
None of these tools replace understanding Docker fundamentals. You still need to know how layers work, why multi-stage builds matter, 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
# Install both toolsbrew install lazydocker dive
# Start your Docker environmentdocker compose up -d
# Open lazydocker to see everythinglazydocker
# In another terminal, analyze an imagedive nginx:latestSpend 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.
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.