Build container images that are reproducible, small, and safe to run — layer order and what actually caches, multi-stage builds, and what belongs in an image versus what must never be baked into one. Use when writing or reviewing a Dockerfile, when an image build is slow, bloated, or non-deterministic, or when deciding how configuration and secrets reach a container. Not for orchestration, cluster or compose topology, not for CI workflow authoring (github-actions), and not for pipeline stage design (ci-cd).
An image is a build artifact with a filesystem attached. Most of what goes wrong with one is
decided by the order of a dozen lines: what got cached, what got copied in that should not have
been, and what the container is running as when it starts.
A built image is not a running service. "The build succeeded" says the layers assembled, nothing
about whether the thing inside starts, serves, or shuts down.
When this fires
A Dockerfile is being written, changed, or reviewed; a build is slow or gives different results on
different machines; an image is unexpectedly large; or config and credentials need a route into the
container. It does not fire for scheduling, networking between services, or a bug in the
application code that happens to be containerized.
Procedure
Read what exists first — the Dockerfile, .dockerignore, and how the image is actually
built (locally, in CI, with what build args). Most "write me a Dockerfile" tasks are really a
fix to the one already there, and a second parallel Dockerfile is how two images drift.
Choose the base deliberately and pin it. Prefer the language's official image or a slim
variant; pin to a digest when reproducibility matters, because a tag moves under you. Alpine
and other musl-based images are smaller but are a different libc: native extensions, DNS
behaviour, and timezone handling can all differ from what you tested on. Take the smaller base
only when you have run the app on it.
Write .dockerignore before you build. Without one, the build context carries .git,
node_modules, local env files, and build output to the daemon on every build — slow, and the
direct cause of a credential ending up inside an image after a broad copy.
Order instructions by rate of change, slowest first: system packages, then dependency
manifests plus the install, then application source last. Copy the manifest and lockfile alone
before the source, so a source edit does not invalidate the dependency layer. Any instruction
invalidates every layer after it — this ordering is the whole of build caching.
Use a multi-stage build so compilers, dev dependencies, and test tooling never reach the
runtime image. Build in one stage, copy only the produced artifact into a clean runtime stage.
This is usually the single largest size reduction available, and it shrinks the attack surface
at the same time.
Keep secrets out of the image entirely. A build argument or environment variable set at
build time is recorded in image metadata. Copying a secret in and deleting it later does not
remove it — the earlier layer still holds it. Use BuildKit's build-time secret mounts, or fetch
the credential at runtime. If a secret was ever baked into an image that has been pushed
anywhere, that is a leaked credential: stop, say so plainly, and treat rotation as the fix.
Install packages in one layer and clean up in the same instruction. Splitting the index
update from the install lets a stale cached index install stale packages; leaving package
manager caches behind inflates the layer permanently, because deleting them later only adds
another layer. Use a build cache mount for package caches you want to reuse across builds.
Run as a non-root user. Create the user, give it ownership of only the paths it must write,
and set USER before the entrypoint. Containers run as root by default and almost nothing needs
it. If the process must bind a low port, change the port rather than keeping root.
Get PID 1 right. Use the exec form of the entrypoint and command so your process is PID 1
and receives signals directly. In shell form the shell is PID 1, SIGTERM is not forwarded, and
the container is killed hard after the grace period — no graceful shutdown, no connection
draining, and it looks fine locally. Add a minimal init process when the app spawns children it
does not reap.
Take configuration at run time, not build time. An image that bakes in an environment's
URLs or feature flags is one image per environment, which breaks build-once-promote: what you
tested is not what you ship. Bake only what is genuinely invariant.
Build it, then look at it. Build clean, then build again unchanged and confirm the cache
behaves as the ordering predicts. Inspect the layer sizes and the final size against the
previous one. Then run it: confirm it starts, serves a real request, exits within a second
or two on SIGTERM, and does so as the non-root user.
Scan before it goes anywhere, and stop there. Pushing to a registry, moving a shared tag
such as latest, and pruning images or volumes on a machine you do not own are all outward or
destructive: prepare them, say exactly what would be pushed or removed, and ask.
Checklist
.dockerignore exists and excludes VCS, local dependencies, env files, and build output
Base image pinned; its libc is one the application has actually run on
Dependency install sits above the source copy, and a source edit reuses it
Build tooling exists only in a build stage, not in the runtime image
No secret in a build arg, an env var, or any layer — checked, not assumed
Explicit non-root USER, with ownership of the paths it writes
Exec-form entrypoint; container observed exiting promptly on SIGTERM
Environment-specific config supplied at run time
Image built, run, and exercised — not just built
Failure handling
The cache never hits. Something high in the file changes every build: a broad COPY above
the dependency install, a timestamp or commit SHA in an early build arg, or a missing
.dockerignore letting the context differ each time. Find the first layer that rebuilds; the
cause is at or above it.
It works locally and fails in CI. Suspect, in order: an unpinned base tag that moved, a file
present locally but ignored or absent in the CI context, a different architecture, and build args
supplied in one place only. Compare the two build contexts before changing the Dockerfile.
The image is still huge after a multi-stage build. Layer size, not final content, is what
counts: a file added and removed in a later instruction is still carried. Look at per-layer sizes
and find the fat one rather than guessing.
The container exits immediately. Read its logs before editing anything. A foreground process
that backgrounds itself, a missing runtime dependency dropped between stages, and a permission
error after adding USER all present the same way.
No way to build or run here. Say the image was written but never built, and never describe it
as working. A Dockerfile that has not been built is a draft.
Evidence to report
The final image size and how it compares to before. The result of the second, unchanged build —
which layers were cached. What the container did when run: startup, one real request served, and
the observed shutdown on SIGTERM. The user id the process runs as. Scanner findings, with
severities, or a statement that no scan was run. And what was not checked — other architectures,
other environments, and registry push, if the push has not happened.
1---2name: docker3description: Build container images that are reproducible, small, and safe to run — layer order and what actually caches, multi-stage builds, and what belongs in an image versus what must never be baked into one. Use when writing or reviewing a Dockerfile, when an image build is slow, bloated, or non-deterministic, or when deciding how configuration and secrets reach a container. Not for orchestration, cluster or compose topology, not for CI workflow authoring (github-actions), and not for pipeline stage design (ci-cd).4---56# Docker images78An image is a build artifact with a filesystem attached. Most of what goes wrong with one is9decided by the order of a dozen lines: what got cached, what got copied in that should not have10been, and what the container is running as when it starts.1112A built image is not a running service. "The build succeeded" says the layers assembled, nothing13about whether the thing inside starts, serves, or shuts down.1415## When this fires1617A Dockerfile is being written, changed, or reviewed; a build is slow or gives different results on18different machines; an image is unexpectedly large; or config and credentials need a route into the19container. It does not fire for scheduling, networking between services, or a bug in the20application code that happens to be containerized.2122## Procedure23241. **Read what exists first** — the Dockerfile, `.dockerignore`, and how the image is actually25 built (locally, in CI, with what build args). Most "write me a Dockerfile" tasks are really a26 fix to the one already there, and a second parallel Dockerfile is how two images drift.272. **Choose the base deliberately and pin it.** Prefer the language's official image or a slim28 variant; pin to a digest when reproducibility matters, because a tag moves under you. Alpine29 and other musl-based images are smaller but are a different libc: native extensions, DNS30 behaviour, and timezone handling can all differ from what you tested on. Take the smaller base31 only when you have run the app on it.323. **Write `.dockerignore` before you build.** Without one, the build context carries `.git`,33 `node_modules`, local env files, and build output to the daemon on every build — slow, and the34 direct cause of a credential ending up inside an image after a broad copy.354. **Order instructions by rate of change**, slowest first: system packages, then dependency36 manifests plus the install, then application source last. Copy the manifest and lockfile alone37 before the source, so a source edit does not invalidate the dependency layer. Any instruction38 invalidates every layer after it — this ordering is the whole of build caching.395. **Use a multi-stage build** so compilers, dev dependencies, and test tooling never reach the40 runtime image. Build in one stage, copy only the produced artifact into a clean runtime stage.41 This is usually the single largest size reduction available, and it shrinks the attack surface42 at the same time.436. **Keep secrets out of the image entirely.** A build argument or environment variable set at44 build time is recorded in image metadata. Copying a secret in and deleting it later does not45 remove it — the earlier layer still holds it. Use BuildKit's build-time secret mounts, or fetch46 the credential at runtime. If a secret was ever baked into an image that has been pushed47 anywhere, that is a leaked credential: stop, say so plainly, and treat rotation as the fix.487. **Install packages in one layer and clean up in the same instruction.** Splitting the index49 update from the install lets a stale cached index install stale packages; leaving package50 manager caches behind inflates the layer permanently, because deleting them later only adds51 another layer. Use a build cache mount for package caches you want to reuse across builds.528. **Run as a non-root user.** Create the user, give it ownership of only the paths it must write,53 and set `USER` before the entrypoint. Containers run as root by default and almost nothing needs54 it. If the process must bind a low port, change the port rather than keeping root.559. **Get PID 1 right.** Use the exec form of the entrypoint and command so your process is PID 156 and receives signals directly. In shell form the shell is PID 1, `SIGTERM` is not forwarded, and57 the container is killed hard after the grace period — no graceful shutdown, no connection58 draining, and it looks fine locally. Add a minimal init process when the app spawns children it59 does not reap.6010. **Take configuration at run time, not build time.** An image that bakes in an environment's61 URLs or feature flags is one image per environment, which breaks build-once-promote: what you62 tested is not what you ship. Bake only what is genuinely invariant.6311. **Build it, then look at it.** Build clean, then build again unchanged and confirm the cache64 behaves as the ordering predicts. Inspect the layer sizes and the final size against the65 previous one. Then **run it**: confirm it starts, serves a real request, exits within a second66 or two on `SIGTERM`, and does so as the non-root user.6712. **Scan before it goes anywhere**, and stop there. Pushing to a registry, moving a shared tag68 such as `latest`, and pruning images or volumes on a machine you do not own are all outward or69 destructive: prepare them, say exactly what would be pushed or removed, and ask.7071## Checklist7273- [ ] `.dockerignore` exists and excludes VCS, local dependencies, env files, and build output74- [ ] Base image pinned; its libc is one the application has actually run on75- [ ] Dependency install sits above the source copy, and a source edit reuses it76- [ ] Build tooling exists only in a build stage, not in the runtime image77- [ ] No secret in a build arg, an env var, or any layer — checked, not assumed78- [ ] Explicit non-root `USER`, with ownership of the paths it writes79- [ ] Exec-form entrypoint; container observed exiting promptly on `SIGTERM`80- [ ] Environment-specific config supplied at run time81- [ ] Image built, run, and exercised — not just built8283## Failure handling8485- **The cache never hits.** Something high in the file changes every build: a broad `COPY` above86 the dependency install, a timestamp or commit SHA in an early build arg, or a missing87 `.dockerignore` letting the context differ each time. Find the first layer that rebuilds; the88 cause is at or above it.89- **It works locally and fails in CI.** Suspect, in order: an unpinned base tag that moved, a file90 present locally but ignored or absent in the CI context, a different architecture, and build args91 supplied in one place only. Compare the two build contexts before changing the Dockerfile.92- **The image is still huge after a multi-stage build.** Layer size, not final content, is what93 counts: a file added and removed in a later instruction is still carried. Look at per-layer sizes94 and find the fat one rather than guessing.95- **The container exits immediately.** Read its logs before editing anything. A foreground process96 that backgrounds itself, a missing runtime dependency dropped between stages, and a permission97 error after adding `USER` all present the same way.98- **No way to build or run here.** Say the image was written but never built, and never describe it99 as working. A Dockerfile that has not been built is a draft.100101## Evidence to report102103The final image size and how it compares to before. The result of the second, unchanged build —104which layers were cached. What the container did when run: startup, one real request served, and105the observed shutdown on `SIGTERM`. The user id the process runs as. Scanner findings, with106severities, or a statement that no scan was run. And what was not checked — other architectures,107other environments, and registry push, if the push has not happened.
Run npx skillmds@latest add nahid-sparktales/docker in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Build container images that are reproducible, small, and safe to run — layer order and what actually caches, multi-stage builds, and what belongs in an image versus what must never be baked into one. Use when writing or reviewing a Dockerfile, when an image build is slow, bloated, or non-deterministic, or when deciding how configuration and secrets reach a container. Not for orchestration, cluster or compose topology, not for CI workflow authoring (github-actions), and not for pipeline stage design (ci-cd). It is listed under DevOps & Infra on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: makes network calls, reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
nahid-sparktales (@nahid-sparktales) published this skill. Their other Agent Skills are listed on their SkillMD profile.