Docker Hands-on Observations: Layer Cache, Restart Behavior, Image Selection, and Resource Limits
DevOps Learning Notes
Hands-on observation notes on Docker fundamentals
Running through several core Docker behaviors, including layer cache hit logic, the difference between container restart and rebuild, image size comparison, and how to verify cgroup resource limits
Legacy Builder vs BuildKit
Modern Docker uses BuildKit as the build engine. The legacy builder has been marked as deprecated.
|
|
Important: The two builders store their caches separately and do not share them. After switching from legacy to BuildKit, the first build will have no cache to use.
BuildKit’s output format is clearer than the old version. Each step indicates whether it hit cache:
|
|
Instruction-Order Effects on Tier Reuse
How Script Sequence Determines Cache Hits
Docker’s cache rule: if the content of a layer changes, all layers below it become invalid and must be re-executed.
This is why you put “things that rarely change” first:
|
|
|
|
Observation Results Across Three Runs
Experiment 1: First build
All layers are actually executed. No CACHED anywhere. Build time is the longest.
Experiment 2: Build again without changing anything
All layers show CACHED. Build time drops from seconds to under 1 second. This is the value of cache.
Experiment 3: Only change source code, don’t touch go.mod
|
|
go mod download hitting cache means packages were not re-downloaded. This is the core reason for separating the COPY steps.
Restarting Containers vs Rebuilding Images
Reloading Does Not Recompile
This is an easily misunderstood concept. Experiment steps:
|
|
Conclusion: restart only stops and re-starts the same process. The binary running inside the container is still the version compiled into the image at build time.
For code changes to take effect, you must go through the full flow:
|
|
Rationale Behind Frozen-at-Start Semantics
At the moment a container starts, it is frozen to that image version. No matter how the image is updated afterwards, running containers are not affected.
This is an intentional design. It makes production environment updates controllable โ things won’t change automatically without your knowledge.
Artifact Footprint: Empty Base Compared to Minimal Runtime
Measured Sizes
|
|
Difference is about 7MB.
Progressive Runtime Additions
|
|
Note: distroless/base is larger than distroless/static, not smaller.
Selection Criteria
The key question is: will your server make outbound HTTPS requests?
“Making outbound requests” means your server calls other services, for example:
- Calling Stripe API for payment
- Calling SendGrid to send email
- Calling Slack to send notifications
- Calling any third-party service
Receiving external requests and returning responses does not count โ that is normal server behavior and does not require CA certificates.
| Scenario | Choice |
|---|---|
| Server only receives requests, no outbound calls | scratch |
| Server needs to make outbound HTTPS requests | distroless/static |
| Language needs runtime (Java, Python) | distroless/java / distroless/python |
Base Image Line Change
|
|
Only the base image is different. Everything else is identical.
Kernel-Enforced Resource Boundaries
Declaring CPU and Memory Ceilings
|
|
--cpus="0.5": use at most 0.5 CPU cores--memory="64m": use at most 64MB of memory
Confirming Constraints Are Active
Method 1: docker inspect
|
|
NanoCpus conversion:
|
|
This number directly corresponds to the Linux kernel’s cgroup setting. Confirming this value confirms the limit is actually applied.
Method 2: docker stats
|
|
|
|
CPU %: current actual CPU usage percentage (near 0% when idle)MEM USAGE / LIMIT: how much memory is currently used / what the limit isMEM %: memory usage percentage
--no-stream purpose: docker stats defaults to a continuously refreshing display (similar to top). Adding --no-stream prints a single snapshot and exits.
Control Groups as a Safety Boundary
Even if the program inside the container goes rogue (infinite loop, memory leak), it can only use up to the configured limit. It will not affect the host or other containers.
This is the Linux kernel’s cgroup enforcement at work. Docker just wraps the configuration into simple parameters.
Checking Published Ports
When running docker run, always verify the PORTS column has a value:
|
|
If PORTS is empty, it means -p 8080:8080 was not applied. You need to stop and recreate the container.
Key Takeaways
-
Layer Cache:
- Dockerfile order determines cache efficiency
- Put things that rarely change first โ package downloads can be cached
-
Container Restart:
restart= restart the same process, binary unchangedrebuild= produce new image, code changes take effect
-
Image Selection:
- scratch โ Go static binary, smallest, no outbound HTTPS needed
- distroless โ needs CA certificates or runtime
- alpine/ubuntu โ for development and debugging, not for production
-
cgroup:
--cpus/--memoryset limitsdocker inspectcheck NanoCpus to confirm settingsdocker statsview real-time resource usage
References
- Docker BuildKit documentation โ official BuildKit documentation, including differences from legacy builder
- Docker build caching โ detailed explanation of layer cache behavior
- Docker resource constraints โ complete reference for
--cpus,--memory, and other resource limit parameters - GoogleContainerTools/distroless โ selection guide for various distroless images
- Linux man page โ cgroups(7) โ kernel-level cgroup documentation