An image describes an ordered filesystem assembled from immutable layer changesets, together with configuration such as its default command. Related images can reference the same base layers while adding different application files. A running container gets its own writable layer above that image. Distinguishing those three things explains how image reuse saves work without making every container’s runtime edits visible to its neighbors.
When it breaks
Summing the displayed sizes of several related images can overstate their combined local storage use because shared content may be counted repeatedly. Transfer size, unpacked filesystem size, and actual storage allocated by a snapshotter are also different measurements. Layer sharing supports reuse, but it does not justify claiming an exact disk saving without measuring the relevant storage implementation and accounting method.
The opposite mistake is treating container changes as edits to the image. Installing a package or changing a file inside one ordinary container modifies that container’s writable filesystem state. A new container from the original image does not automatically inherit the repair. An operator who patches a running service and then replaces it can therefore see the original failure return.
Sharing does not make an image layer a universal application compatibility boundary. Two applications might use equal base layers yet depend on different configuration, mounted data, architectures, or library versions introduced later. Compare the ordered filesystem identifiers and the actual behavior under test; a matching readable image tag alone says little about which bytes or settings are in use.
Check it quickly
Run this script with Python and a Docker builder after obtaining the pinned Python base digest. The recorded environment used Docker 29.3.1 on Linux arm64, with Python 3.11.16 inside the image. It creates two related images and two temporary containers, uses no container networking, and removes only its own named containers and tags when finished:
from pathlib import Path
from tempfile import TemporaryDirectory
import json,subprocess,uuid
BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84'
def docker(*args,check=True):
result=subprocess.run(["docker",*args],capture_output=True,text=True,timeout=120)
if check:assert result.returncode==0,result.stderr
return result
def inspect(image):return json.loads(docker("image","inspect",image).stdout)[0]
name='xbcatm-layers-'+uuid.uuid4().hex[:10];tags=[name+':left',name+':right'];containers=[name+'-one',name+'-two']
try:
with TemporaryDirectory(prefix=name+'-') as directory:
root=Path(directory);(root/'shared.py').write_text('value=10\n# '+name+'\n')
(root/'Dockerfile').write_text(f'FROM {BASE}\nCOPY shared.py /app/shared.py\nCOPY main.py /app/main.py\nCMD ["python", "/app/main.py"]\n')
for tag,increment in zip(tags,[1,2]):
(root/'main.py').write_text(f'from shared import value\nprint(value+{increment})\n')
docker('build','--network','none','-t',tag,str(root))
left=inspect(tags[0])['RootFS']['Layers'];right=inspect(tags[1])['RootFS']['Layers']
assert left[:-1]==right[:-1] and left[-1]!=right[-1]
assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='11'
assert docker('run','--rm','--network','none',tags[1]).stdout.strip()=='12'
print('two images share',len(left)-1,'filesystem layer identities and differ in the final application layer')
for container in containers:
docker('run','-d','--name',container,'--network','none',tags[0],'python','-c','import time;time.sleep(60)')
docker('exec',containers[0],'python','-c',"from pathlib import Path;Path('/app/shared.py').write_text('value=100\\n')")
assert docker('exec',containers[0],'python','/app/main.py').stdout.strip()=='101'
assert docker('exec',containers[1],'python','/app/main.py').stdout.strip()=='11'
assert '/app/shared.py' in docker('diff',containers[0]).stdout
assert inspect(tags[0])['RootFS']['Layers']==left
assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='11'
print('a write in one container changes its writable layer while the sibling container and original image retain the old code')
finally:
docker('rm','-f',*containers,check=False);docker('image','rm',*tags,check=False)
Both builds copy the same shared.py, which defines a value of ten. Their final main.py differs: one adds one and the other adds two. The resulting programs print 11 and 12. Image inspection shows an identical ordered prefix of five filesystem layers and a different final application layer in this recorded base and build arrangement.
The five-layer count is an observation, not a requirement for every Python image. The meaningful assertion compares all layers before the final COPY and confirms that the last identifiers differ. If the publisher changes the base structure, a new digest may have another number of layers while the same sharing principle continues to apply.
Next, two long-running containers start from the first image. A command changes /app/shared.py in only the first container to define one hundred. Executing its application now prints 101. Executing the sibling’s application still prints 11, and docker diff reports the changed path in the edited container’s writable state.
The original image retains its layer identifiers. A fresh container created from that image also prints 11, proving that the runtime edit was not written back into the image. The experiment uses ordinary image-backed paths with no shared volume or host bind mount. A shared writable mount would have a different visibility contract and must be examined separately.
The inspected RootFS.Layers list identifies uncompressed filesystem changesets through DiffIDs. Those values should not be casually substituted for registry manifest digests, compressed layer-blob digests, or image configuration digests. All are content identifiers, but they identify different serialized objects and serve different purposes in storage, retrieval, and artifact selection.
Build and replace the intended artifact
Keep common dependency inputs stable when related applications can actually share them, then copy application-specific code later. This improves the opportunity for cache and content reuse without requiring that unrelated applications use an artificial common dependency set. Correct dependency boundaries come from the software, not from maximizing the number of matching hash strings.
Turn a necessary runtime repair into a reviewed image change and test a replacement container. Persist durable application data through an explicit storage contract; do not rely on the writable layer surviving replacement. Conversely, do not assume deleting one container removes data held in an independently managed volume.
For operational accounting, measure shared and unique storage with tools appropriate to the builder and image store. For release identity, record the artifact reference and runtime configuration. For debugging, inspect both the image and container differences. These observations answer related questions, but no single image-size column or tag answers all of them.
Sources
- Docker image layers, shared read-only layers and container writable layers, checked 2026-09-12.
- OCI image configuration 1.1.1, ordered filesystem DiffIDs and their distinction from other digests, checked 2026-09-12.
- Optimize Docker cache usage, dependency ordering and narrow build inputs, checked 2026-09-12.