Docker’s CLI Is Technically Fine (and That’s the Problem)
Let’s get something out of the way: docker ps, docker logs, and docker stats all work. They’re functional. They get the job done the way a spoon technically works as a screwdriver. Nobody’s stopping you, but you’re making life harder than it needs to be.
The Docker CLI was built by people who think in composable Unix commands and pipe chains, which is great if you’re writing shell scripts. But if you’re a human who just wants to see what’s running, check some logs, and restart a container without typing four separate commands, you deserve better tools.
Enter lazydocker and dive, two CLI tools that approach Docker from completely different angles but share the same philosophy: you shouldn’t need a photographic memory of Docker’s 50+ subcommands to be productive.
Lazydocker gives you a full terminal dashboard for managing containers, images, volumes, and networks. Dive lets you crack open an image and inspect every layer to find 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.
Lazydocker: A Full Docker Dashboard in Your Terminal
Lazydocker is from Jesse Duffield, who also made lazygit, which tells you something about his patience for typing long commands. It’s a terminal UI that gives you a real-time view of everything Docker is doing on your system: think docker ps + docker logs + docker stats + docker compose, fused into one interface you navigate with a keyboard.
Installing Lazydocker
brew install lazydocker # Homebrew (macOS/Linux)go install github.com/jesseduffield/lazydocker@latest # Go 1.21+nix-env -iA nixpkgs.lazydocker # Nixcurl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bashOr run it in Docker, because of course you can:
docker run --rm -it \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/.config/lazydocker:/.config/jesseduffield/lazydocker \ lazyteam/lazydockerOnce installed, type lazydocker in any terminal.
The Interface
Lazydocker opens a multi-panel layout familiar to anyone who’s used tmux or a tiling window manager. The left sidebar lists containers, images, volumes, and networks (switch categories with [ and ]). The top-right panel shows details for whatever’s selected, defaulting to logs for containers. The bottom-right panel shows stats, environment variables, or other secondary info. Everything updates live: CPU and memory graphs animate, logs stream as they happen, all without six terminal tabs.
Essential Keybindings
| 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) |
x | Open context menu for current item |
/ | Filter |
The x key is the one to remember. It opens every action available for whatever you’ve selected.
Customizing Lazydocker
The config lives at ~/.config/lazydocker/config.yml:
gui: scrollHeight: 2 theme: activeBorderColor: [green, bold]
reporting: "off"
commandTemplates: dockerCompose: "docker compose"
logs: timestamps: true since: "60m" tail: "200"logs.since matters more than it looks: the default pulls all logs on open, which is painfully slow for chatty containers. Set commandTemplates.dockerCompose to docker compose (with a space) for Compose V2; the standalone docker-compose binary is end of life.
Custom Commands: The Secret Weapon
customCommands: containers: - name: "View container IP" command: "docker inspect --format '{{ .NetworkSettings.IPAddress }}' {{ .Container.ID }}" images: - name: "Dive into image" attach: true command: "dive {{ .Image.ID }}"That last one lets you select an image in lazydocker and jump straight into dive for layer analysis, which brings us to the other tool.
Dive: X-Ray Vision for Docker Images
Dive explores Docker images, their layer contents, and where all that disk space went. If you’ve 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 inside them easily. docker history gives you a vague summary. docker inspect gives you JSON that requires a law degree to parse. Dive gives you an actual file browser: walk through each layer and see exactly what was added, modified, or removed.
Installing Dive
brew install divego install github.com/wagoodman/dive@latestDIVE_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"Using Dive
dive nginx:latestOr build and analyze in one step, which runs docker build and opens the result immediately:
dive build -t my-app:latest .Dive’s two-panel layout puts layers on the left (each mapped to a Dockerfile instruction, with size and running total) and a color-coded file tree on the right: green for added, yellow for modified, red for removed (still taking space in the image), white for unchanged.
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 | Collapse/expand all directories |
Space | Collapse/expand a directory |
Ctrl+L | Toggle between current-layer view and cumulative (aggregated) view |
Ctrl+F | Filter files |
Ctrl+F is the one you’ll reach for most. Looking for that mystery .cache directory eating 400 MB? Filter for it.
Practical Walkthrough: Why Is This Image So Big?
Say you have this Dockerfile:
FROM node:22WORKDIR /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 for 200 lines of TypeScript. Run dive build -t my-app:debug . and here’s what you’ll typically find:
- Layer 1 (
FROM node:22): ~350 MB. The full Node.js runtime on Debian. Already a chonker. - Layer 3 (
npm install): ~450 MB. Yournode_modulesin all its glory. - Layer 4 (
COPY .): ~200 MB. Wait, that copiednode_modulesagain? And.git? And a 150 MB test fixture you forgot about? - Layer 5 (
npm run build): ~50 MB. The actual build output.
Dive makes all of it obvious at a glance: the duplicate node_modules, the .git directory with no business in a production image, the stray test data. Armed with that, you rewrite it as a multi-stage build:
FROM node:22-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run buildRUN npm prune --omit=dev
FROM node:22-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 at roughly 180 MB. That’s the value of actually seeing what’s in your layers instead of guessing.
Dive’s Image Efficiency Score
At the bottom of the dive UI sits an efficiency score: how much wasted space exists in your image (files added in one layer, removed in another, but still taking up space because that’s how layers work). 100% means no waste. 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 (the cleanup doesn’t save space because it’s a new layer), copying files in one layer and deleting them in another, or installing dev dependencies and pruning them later instead of never installing them in the final stage. The fix is almost always combining operations into 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 only interactive. Run it in CI with CI=true dive <image> --ci-config .dive-ci.yml to fail builds automatically when images bloat past a threshold.
rules: lowestEfficiency: 0.9 highestWastedBytes: 50mb highestUserWastedPercent: 0.15GitHub Actions
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.ymlGitLab CI
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.ymlNow every PR touching the Dockerfile or dependencies gets checked automatically. No more “we’ll optimize it later” that never happens.
Bonus Tool: DockerSlim (Now Just Slim)
If lazydocker is your dashboard and dive is your x-ray machine, Slim is the surgeon. It launches your container in a sandbox, monitors which files are actually accessed at runtime, and builds a new minimal image containing only those files. A 300 MB image might slim down to 30 MB because 90% of what’s in there was never touched.
curl -sL https://raw.githubusercontent.com/slimtoolkit/slim/master/scripts/install-slim.sh | sudo -E bash -
slim build --target my-app:latest --tag my-app:slim
# HTTP probing exercises more code paths for web appsslim build --target my-app:latest --tag my-app:slim \ --http-probe-cmd /health --http-probe-cmd /api/status --expose 3000The Slim + Dive Workflow
The real power move is using 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 minify what’s left.
- Run dive again on the slim output to verify.
docker build -t my-app:latest .dive my-app:latest# (optimize Dockerfile based on findings)slim build --target my-app:latest --tag my-app:slimdive my-app:slimdocker images | grep my-appA word of caution: slim can break things if your app accesses files dynamically or at paths the probe never exercised. Timezone data, SSL certificate bundles, and locale files are the usual casualties. Always run your full test suite against a slimmed image before shipping it, and use --include-path to explicitly keep anything the probe missed.
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, and what .dockerignore does. They just shrink the gap between knowing the concepts and actually applying them.
Quick Start: Get Running in 5 Minutes
brew install lazydocker divedocker compose up -dlazydocker# in another terminaldive nginx:latestSpend ten minutes clicking around lazydocker, then switch to dive and explore a few images. Once you’ve used both for a day, the raw Docker CLI feels like going back to dial-up.
Final Thoughts
Docker’s learning curve isn’t really about the concepts, it’s about managing complexity as you add more containers, images, and services. The raw CLI scales poorly for humans. Lazydocker turns management from a memory test into a visual experience. Dive turns image optimization from guesswork into a repeatable check. Install them, use them, and stop typing docker logs --tail 100 -f that-container-with-the-really-long-name from memory.