Podman Image Layers and Build Cache
Rebuilding an unchanged Containerfile produces the same image ID, so `app:1` and `app:2` are one image with two tags. Change the copied file and the cache breaks at `COPY` and stays broken for every instruction below it.
Building Images Guide 18 of 47 Intermediate
- OSUbuntu 26.04 LTS (resolute)
- Podman5.7.0
- Runtimecrun 1.21
- Networknetavark 1.16.1
- Buildah1.42.1
- TimeAbout 12 min
- Reviewed22 August 2026
Written against the versions above. Podman follows the distribution here rather than a vendor repository, so the version you get is the one Ubuntu shipped. The commands are stable across 5.x.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| PODMAN01 | 192.168.0.24 | Ubuntu 26.04 LTS | Primary Container Host | 2 Core | 4 GB | 50 GB |
Before you start
- guide 30 - the same
~/appbuild context.
-
The same build twice
Change nothing and build again under a new tag. Every step reports:
--> Using cache 6180d06f43b1292a5b2b48b89ab2c48f...The expensive
apk adddid not run. Neither did anything else.Then read the last lines carefully - the build reports
Successfully tagged localhost/app:2andlocalhost/app:1, and prints the same final ID as before. Nothing was created. You added a second tag to the image you already had.The cache key is what an instruction *would produce*: the parent layer plus the instruction text plus, for
COPY, the contents of the files. Identical inputs mean an identical layer, so there is no reason to build one.bash Example session cd ~/app && podman build -t app:2 .STEP 1/5: FROM docker.io/library/alpine:3.22STEP 2/5: RUN apk add --no-cache curl--> Using cache 6180d06f43b1292a5b2b48b89ab2c48ffded5f0f56c1d4981891188f1bacc8ad--> 6180d06f43b1STEP 3/5: COPY hello.sh /usr/local/bin/hello--> Using cache 1f4fdfd3f2281e8afa6a39098bd3dd3721db1291dcfd3f4b1b7a90e96258326c--> 1f4fdfd3f228STEP 4/5: RUN chmod +x /usr/local/bin/hello--> Using cache ab7f293673bf308fe705a9cfc7a571a3c82491eee09dc5f04ecb9665ff101241--> ab7f293673bfSTEP 5/5: CMD ["/usr/local/bin/hello"]--> Using cache c154b5c3995cb4c11724e001b4be0eec6051a98131ed502f345445293ac8f92fCOMMIT app:2--> c154b5c3995cSuccessfully tagged localhost/app:2Successfully tagged localhost/app:1Expected result
Using cacheon every step, and the same final image ID asapp:1.Success conditionA rebuild produced no new image.
-
Break it, and watch the cascade
Change one line in
hello.shand rebuild:STEP 2/5: RUN apk add --no-cache curl --> Using cache 6180d06f43b1... <- still cached STEP 3/5: COPY hello.sh /usr/local/bin/hello --> 9779093186b3 <- rebuilt STEP 4/5: RUN chmod +x /usr/local/bin/hello --> 1582b6a9092a <- rebuilt tooStep 2 is still cached because nothing above it changed. Step 3 rebuilt because the file contents are part of its key. And step 4 rebuilt even though its instruction text is identical, because its parent layer changed - and a layer's identity includes what it was built on.
That cascade is the single most useful thing to know about build performance. The cache is not per-instruction, it is a chain: the first instruction whose inputs change invalidates everything below it.
Which gives the ordering rule. Put the instructions that rarely change at the top and the ones that change every commit at the bottom.
COPYyour dependency manifest and install dependencies, *then* copy your source - because source changes on every build and dependencies do not.bash Example session cd ~/app && printf '#!/bin/sh\necho "hello again from $(hostname)"\n' > hello.shcd ~/app && podman build -t app:3 .STEP 1/5: FROM docker.io/library/alpine:3.22STEP 2/5: RUN apk add --no-cache curl--> Using cache 6180d06f43b1292a5b2b48b89ab2c48ffded5f0f56c1d4981891188f1bacc8ad--> 6180d06f43b1STEP 3/5: COPY hello.sh /usr/local/bin/hello--> 9779093186b3STEP 4/5: RUN chmod +x /usr/local/bin/hello--> 1582b6a9092aSTEP 5/5: CMD ["/usr/local/bin/hello"]COMMIT app:3--> 3add4ec8193bSuccessfully tagged localhost/app:33add4ec8193b5c90f4bdbfad26fa4ba259222ea21f5bcb500daba13d4ca11444podman run --rm app:3hello again from 93362a62e3a9Expected resultStep 2 cached, steps 3 and 4 rebuilt, and a new final image ID.
Success conditionYou can predict which steps a given edit will invalidate.
-
Two tags, one image, and one different
The image list makes the whole lesson visible at once:
localhost/app 3 3add4ec8193b localhost/app 2 c154b5c3995c localhost/app 1 c154b5c3995capp:1andapp:2share an ID - one image, two names, occupying one image's worth of disk.app:3is genuinely different.Practical consequences worth holding onto:
- A tag is not a version. Two tags can be the same bytes, and one tag can be moved to different bytes tomorrow. When it has to be the same image later, record the digest - see guide 12.
podman rmi app:1will not free space whileapp:2points at the same image; it just removes a name.- A build that produces no new ID did nothing, which is a fast way to check whether the change you thought you made actually reached the context.
bash Example session podman images --format 'table {{.Repository}} {{.Tag}} {{.ID}}' appREPOSITORY TAG IMAGE IDlocalhost/app 3 3add4ec8193blocalhost/app 2 c154b5c3995clocalhost/app 1 c154b5c3995cExpected resultThree rows, two of which share an image ID.
Success conditionYou can tell from an image list which tags are the same image.
Troubleshooting
Nothing is ever cached, on every build.
Why: Something near the top changes every time - a timestamp in an
ARG, aCOPY .that includes a file written by the build itself, or--no-cacheleft in a script.Fix:Move volatile inputs down. A
COPY . .high in the file is the usual culprit; narrow it or add a.containerignore.A cached layer is stale - the package index is months old.
Why:
RUN apk addis cached on its instruction text, which has not changed. The cache cannot know the remote index moved.Fix:
podman build --no-cache, or pin versions so the text changes when the intent does. This is the trade-off the cache makes, not a bug.The build cached a step you expected to rerun after editing a file.
Why: The file is not in the build context, so
COPYnever saw your edit.Fix:
lsthe context directory. Also check.containerignoreis not excluding it.podman rmisays the image is in use by another tag.Why: Several tags point at one image ID.
Fix:Remove the tags individually, or
podman rmi -f <id>to drop them all at once.