Image Management
Every docker image build or docker container commit, every docker image pull, every failed experiment leaves something behind. None of it announces itself — it just quietly stacks up until your disk starts complaining.
The good news: managing images is one of the easiest habits to build once you know the handful of commands that matter. In this lesson, we’ll go through listing images, inspecting them to understand what’s actually inside, and cleaning up the mess without accidentally nuking something you needed.
Every command here lives under the same modular family — docker image <something> — which makes it obvious we’re always playing in the same sandbox: images, and nothing else.
flowchart LR
A[docker image pull / build] --> B[Image stored locally]
B --> C{Still needed?}
C -->|Yes| D[Keep & use]
C -->|No| E[Remove / Prune]
D --> F[Container runs]
E --> G[Disk space reclaimed]
Listing and Managing Images
The starting point for almost everything in this article is one command:
docker image lsYou might also see the older alias docker images floating around in tutorials and Stack Overflow answers — it does the exact same thing, but we’ll stick to the modular docker image family throughout this lesson. It groups every image-related action under one predictable namespace (ls, inspect, history, rm, prune…), so once you know the pattern, half the commands are guessable before you even look them up.
A typical output looks like this:
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 2 weeks ago 141MB
myapp v1.2 3f4d9a1e2b3c 3 days ago 512MB
myapp <none> a1b2c3d4e5f6 3 days ago 510MB
postgres 14 4d0c4a5b6c7d 1 month ago 376MBThat <none> tag is worth pointing out early because it confuses a lot of people — it usually means a dangling image: an old layer that got orphaned when you rebuilt an image with the same tag. It still takes up disk space, it’s just no longer reachable by name. We’ll deal with those in the cleanup section.
Filtering the List
Once you’ve got more than a handful of images, scrolling through the full list stops being useful. --filter narrows things down:
# Only dangling images
docker image ls --filter "dangling=true"
# Only images matching a repository name
docker image ls --filter "reference=myapp*"
# Images created before a specific one
docker image ls --filter "before=myapp:v1.2"
# Images created after a specific one
docker image ls --filter "since=postgres:14"You can stack multiple --filter flags, and Docker will AND them together.
Formatting the Output
By default, docker image ls gives you a fixed table. If you want to script against it, or just prefer a cleaner view, --format lets you use Go templates:
docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"REPOSITORY TAG SIZE
nginx latest 141MB
myapp v1.2 512MB
postgres 14 376MBOr skip the table headers entirely and pull just the IDs — handy when piping into another command:
docker image ls -q-q (quiet mode) is one of the most useful flags across Docker’s CLI. It shows up again and again when you want to feed image IDs into another command, like docker image rm $(docker image ls -q -f dangling=true).Tagging: Giving Images Names That Make Sense
Listing images is only half the story — how you name them determines how easy that list is to read six months from now. docker image tag lets you add a new name (and optionally a new tag) to an existing image without duplicating any data:
docker image tag myapp:v1.2 myregistry.com/myapp:v1.2
docker image tag myapp:v1.2 myapp:latestBoth tags point at the same underlying image ID — Docker is just adding labels, not copying bytes. This is exactly how you prep an image for pushing to a private registry under a different name.
Inspecting Images
Listing tells you an image exists. Inspecting tells you what’s actually inside it — the entrypoint, the exposed ports, the environment variables baked in, the layers that make up its size. You’ll reach for this constantly when debugging “why is this container behaving differently than I expect” or “why is this image 2GB when it should be 200MB.”
docker image inspect: The Full Metadata Dump
docker image inspect nginx:latestThis returns a large JSON blob — config, layers, environment variables, exposed ports, volumes, labels, the works. It’s comprehensive but noisy, so you rarely want the whole thing. Use --format (Go templates again) to pull just what you need:
# What command does this image run by default?
docker image inspect --format '{{.Config.Cmd}}' nginx:latest
# What ports does it expect to expose?
docker image inspect --format '{{.Config.ExposedPorts}}' nginx:latest
# What environment variables are baked in?
docker image inspect --format '{{.Config.Env}}' nginx:latest
# How big is it, in bytes?
docker image inspect --format '{{.Size}}' nginx:latestWhen to reach for inspect: debugging why a container isn’t listening on the port you expect, checking whether an environment variable you thought you set actually made it into the image, or confirming the working directory / entrypoint before you override it at docker run time.
docker image history: Understanding the Layers
inspect tells you the final state. docker image history tells you how you got there — every layer, in order, with the command that created it and how much size it added:
docker image history myapp:v1.2IMAGE CREATED CREATED BY SIZE
3f4d9a1e2b3c 3 days ago CMD ["node" "server.js"] 0B
<missing> 3 days ago RUN npm install 340MB
<missing> 3 days ago COPY package.json . 1.2kB
<missing> 3 days ago FROM node:18-alpine 170MBThis is your best tool for hunting down why an image is bloated. If you see a RUN apt-get update && apt-get install ... layer that’s 800MB, that’s your culprit — probably because build tools or package caches weren’t cleaned up in the same layer they were installed in.
flowchart TB
subgraph Image["myapp:v1.2 — layer stack"]
direction TB
L1["FROM node:18-alpine (170MB)"]
L2["COPY package.json . (1.2kB)"]
L3["RUN npm install (340MB)"]
L4["CMD node server.js (0B)"]
L1 --> L2 --> L3 --> L4
end
A common pattern once you spot a bloated layer: combine install-and-cleanup into a single RUN step so the cache never gets baked into a separate layer:
# Before — cache persists in its own layer
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# After — cleanup happens within the same layer
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*RUN step doesn’t shrink the image — the file still exists in the earlier layer, it’s just hidden by an “upper” layer marking it deleted. If size matters, clean up in the same layer where the bloat was introduced.Comparing Two Images Quickly
Sometimes you just want to know what changed between two tags. There’s no single built-in diff command for images, but a quick one-liner gets you close:
diff <(docker image inspect myapp:v1.1) <(docker image inspect myapp:v1.2)Removing and Cleanup Images
This is the section that actually gets your disk space back. Docker gives you a spectrum of tools here, from surgical (remove one specific image) to broad (clear out everything unused) — all still under the same docker image family.
Removing Specific Images
docker image rm myapp:v1.1If the image is in use by a stopped container, Docker will refuse and tell you so — which is a good thing, it’s protecting you. You can force it, but be sure that’s actually what you want:
docker image rm -f myapp:v1.1Removing by ID works the same way, and you can pass multiple targets at once:
docker image rm 3f4d9a1e2b3c a1b2c3d4e5f6Removing Dangling Images
Remember those <none> tagged images from earlier? Clear them out specifically:
docker image pruneThis only touches dangling images by default — it will not remove tagged images you’re not using, so it’s the safest cleanup command in the list.
Removing All Unused Images
If you want to be more aggressive — removing every image not currently tied to a running or stopped container — add -a:
docker image prune -aThis is where people get nervous, and rightly so: it will remove images you pulled last week for a project you haven’t touched since. Docker will prompt for confirmation unless you pass -f.
flowchart TD
Start["Need to free up image disk space?"] --> Q1{"Know the exact image?"}
Q1 -->|Yes| RM["docker image rm image_name"]
Q1 -->|No| Q2{"Just want dangling images gone?"}
Q2 -->|Yes| Prune["docker image prune"]
Q2 -->|No| Q3{"Comfortable removing all unused images?"}
Q3 -->|Yes| PruneA["docker image prune -a"]
docker image prune -a is scoped to images only, but it’s still irreversible — anything pulled or built without a running/stopped container attached to it gets removed. Run docker image ls right before to sanity-check what’s about to disappear.Checking Reclaimable Space, the Image-Scoped Way
Rather than reaching for a broader disk-usage command, you can get a quick sense of what’s reclaimable just by listing sizes and cross-referencing with what’s actually in use:
docker image ls --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"Pair that with docker image ls --filter dangling=true to see exactly which entries are pure waste versus tagged images you’re simply not using right now. It’s a smaller, more deliberate view than a full disk report — appropriate, since we’re only managing images here, not the whole Docker footprint.
Automating Cleanup
If you’re running Docker on a build server or a machine that pulls/builds images constantly, doing this manually gets old fast. A simple weekly cron job keeps things in check without needing to think about it:
# crontab -e
0 3 * * 0 docker image prune -a -f --filter "until=168h"This runs every Sunday at 3 AM and removes unused images older than a week (168h), leaving anything recent alone in case you’re still actively iterating on it.
docker image commands. If you’re looking for a broader reset — clearing stopped containers, unused networks, and build cache all at once — that’s docker system prune, and it deserves (and gets) a lesson of its own since it touches far more than just images.Best Practices for Staying Ahead of Image Bloat
A little discipline at build time saves you from most of this cleanup in the first place:
- Avoid
:latestin production. It’s convenient locally, but it makes it impossible to tell which build is actually running, and it complicates cleanup since every rebuild silently orphans the previous “latest.” - Use multi-stage builds to keep build tools and intermediate artifacts out of your final image entirely, rather than installing and then trying to clean them up in place.
- Add a
.dockerignorefile. Anything you don’t explicitly need in the build context (.git,node_modules, local env files) shouldn’t be sent to the daemon in the first place — it slows builds and can leak into layers. - Order your Dockerfile instructions from least to most frequently changing. Dependencies before source code, so Docker’s layer cache actually gets reused between builds instead of invalidating everything on every code change.
- Tag deliberately. Version tags, git-SHA tags, or date-based tags all make it obvious later which images are safe to prune and which ones someone might still depend on.
Quick Reference and Cheatsheet
| Goal | Command |
|---|---|
| List all images | docker image ls |
| List only dangling images | docker image ls -f dangling=true |
| Inspect one field of an image | docker image inspect --format '{{.Config.Env}}' <image> |
| See layer-by-layer history | docker image history <image> |
| Tag an image | docker image tag <image> <new-name> |
| Remove one image | docker image rm <image> |
| Force remove | docker image rm -f <image> |
| Remove dangling images only | docker image prune |
| Remove all unused images | docker image prune -a |
Wrapping Up
Image management isn’t glamorous, but it’s one of those habits that pays for itself the first time you don’t run out of disk space in the middle of a deploy. The core loop is simple: list to see what you have, inspect when something’s behaving unexpectedly, and prune on a schedule so cleanup is never a fire drill.