VMware ESXi and vSphere Cluster Management

Dockerfile: Build Reproducible Docker Images

Learn Dockerfile syntax, build context, image layers, caching, multi-stage builds, security, debugging, and production optimization with practical examples.

A Dockerfile is a declarative text file containing instructions for creating a container image. A container image is an immutable packaged filesystem and runtime configuration. A container is a runnable instance of an image with runtime settings and a writable layer.

Dockerfile fundamentals

The usual filename is Dockerfile, with no file extension. Docker reads it from the build context by default. Use -f to select another filename or location:

docker build -t example-app:1.0 .
docker build -f docker/Dockerfile -t example-app:1.0 .

The final argument is the build context: the directory or source set sent to the builder. The Dockerfile location and context are separate concepts. In the second command, the Dockerfile is under docker/, but the context is the current directory, represented by ..

Instructions are evaluated in order. They can create filesystem layers or change image configuration such as the default command, user, environment, or exposed ports. A normal lifecycle is:

  1. Write a Dockerfile and supporting files.
  2. Build an image from a context.
  3. Inspect its history and configuration.
  4. Run a container from the image.
  5. Iterate after testing or diagnosing the result.

Build context and file selection

COPY and most local uses of ADD can read only files available in the build context. They cannot freely access a parent directory merely because the Dockerfile is stored there. Use a context that contains exactly the source files the build needs.

A .dockerignore file excludes patterns from the context before it is sent to the builder. This reduces transfer time, improves cache behavior, limits accidental disclosure, and makes builds more reproducible.

.git
.gitignore
node_modules/
__pycache__/
.venv/
.env
.env.*
coverage/
dist/
build/
*.pem
*.key
*.token

Do not place credentials, private keys, generated output, local dependency directories, test reports, or version-control metadata in the context unless they are intentionally required. Check both the selected context and .dockerignore when a COPY source is missing.

Core Dockerfile instructions

InstructionPrimary purposeBuild-time or runtime effectKey cautionsTypical example use
FROMSelect a base imageStarts a stagePrefer maintained tags or digests; scratch has no shell or librariesFROM python:3.12-slim
RUNExecute build commandsCreates a layerClean temporary data in the same stepInstall packages or compile code
COPYCopy context files or stage artifactsCreates a layerPaths are relative to context; use ownership options where supportedCOPY --chown=app:app . .
ADDCopy files with extra behaviorCreates a layerCan extract local archives and retrieve remote sources; behavior can surprise reviewersDeliberate archive extraction
WORKDIRSet the working directoryChanges later build and default runtime directoryPrefer it to repeated cd commandsWORKDIR /app
ENVSet environment variablesPersists in image configuration and runtimeDo not use for secretsENV APP_ENV=production
ARGAccept build parametersAvailable during build only unless copied into ENVValues can appear in build metadata; not secret storageARG VERSION=1.0
EXPOSEDocument intended portsImage metadata onlyDoes not publish a host portEXPOSE 8080
USERSelect the build and runtime userAffects later instructions and default processEnsure files and writable directories have suitable ownershipUSER app
CMDSet default command or argumentsRuntime configurationOnly the last CMD is effective; easily overriddenCMD ["./server"]
ENTRYPOINTSet the primary executableRuntime configurationOnly the last one is effective; shell form handles signals poorlyENTRYPOINT ["./server"]
LABELAttach metadataImage configurationKeep values useful and non-sensitiveSource, version, license
VOLUMEDeclare a mount pointRuntime and image metadataData behavior changes when a volume is mountedVOLUME ["/var/lib/app"]
SHELLChange shell for shell-form instructionsBuild behaviorUse only when shell semantics require itSHELL ["/bin/bash", "-c"]
HEALTHCHECKReport service healthRuntime health stateA check must test real readiness, not merely process existenceHEALTHCHECK CMD curl -f http://localhost:8080/health
STOPSIGNALChoose the stop signalRuntime behaviorUse a signal the application handles correctlySTOPSIGNAL SIGTERM
ONBUILDDefer an instruction to child imagesDescendant build behaviorUse deliberately because effects are invisible until inheritanceONBUILD COPY . /src

Base images and reproducibility

FROM selects the parent image. Tags such as python:3.12-slim are readable but mutable. An image digest identifies content immutably:

FROM python:3.12-slim@sha256:<verified-digest>

Pin important production inputs, update them deliberately, and verify compatibility, certificates, timezone data, shared libraries, and security support. The special scratch base is empty. It can suit a statically linked binary, but provides no shell, certificates, users, or runtime libraries.

RUN, COPY, and ADD

RUN executes at build time. Shell form invokes a shell, while exec form starts the program directly:

RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
RUN ["/usr/bin/my-builder", "--release"]

COPY is the normal choice for local files and can copy from a named build stage with --from. Ownership options such as --chown=app:app avoid a later ownership correction when supported by the builder. ADD additionally supports local archive extraction and remote sources. Prefer COPY unless that extra behavior is intentional and documented.

WORKDIR, ENV, ARG, USER, and metadata

WORKDIR establishes the directory for following instructions and the default runtime directory. ENV persists in image configuration and is available in a running container, although runtime options can override it. ARG is a build-time variable with optional defaults; an argument declared before FROM is available for the FROM expression, while stage-specific arguments must be declared in that stage.

CharacteristicARGENV
Available during buildYes, after declarationYes, after declaration
Available in a running containerNot automaticallyYes
Persistence in image configurationNot as runtime environment, but values may appear in metadata or historyYes
Suitable for secretsNoNo
Scope behaviorStage-scoped; pre-FROM arguments need redeclaration in a stageApplies from its declaration onward
Override mechanismdocker build --build-arg NAME=valuedocker run -e NAME=value

Create an application user and switch to it with USER whenever possible. LABEL can record a source repository identifier, version, license, or build information, but never credentials. VOLUME documents a mount point and may cause Docker to create an anonymous volume when no explicit mount is supplied; persistent data should normally be managed explicitly at runtime.

Shell form, exec form, and startup behavior

Shell form is written as ordinary text, for example CMD python app.py. It is interpreted through a shell and supports shell operators such as pipes and variable expansion. Exec form is JSON-array syntax, for example CMD ["python", "app.py"]; it directly specifies executable arguments.

Dockerfile variable replacement, shell expansion, and runtime environment expansion are different stages. Docker may replace variables known while processing an instruction; a shell may expand variables when a shell-form command runs; and a runtime ENV is available only when the container starts. Quote JSON-form arrays correctly and use backslashes for Dockerfile line continuations. A line beginning with # is a comment. Parser directives such as # syntax=... must appear at the beginning and affect parsing; use only directives supported by your builder.

ENTRYPOINT formCMD formResulting startup behaviorHow docker run arguments affect itRecommended use case
ExecExecEntrypoint executable receives CMD argumentsArguments replace CMD arguments and are appended to entrypointMost services and CLIs
ExecShellCMD becomes a single default argument string in less predictable combinationsUsually avoid for direct argument compositionRare, deliberate wrapper behavior
ShellAnyShell becomes the effective processArguments generally do not receive the intended direct compositionCommands requiring shell syntax
NoneExec or shellCMD is the default executable and argumentsRuntime arguments replace CMDSimple images

Only one effective CMD and one effective ENTRYPOINT remain: a later declaration replaces an earlier declaration. Exec-form ENTRYPOINT is important for long-running services because the application becomes the main process and receives stop signals directly. Shell wrappers should end with exec "$@" so signals reach the actual application.

Building and running images

docker build -t example-app:1.0 .
docker run --rm example-app:1.0
docker run --rm -p 8080:8080 -e APP_ENV=production example-app:1.0
docker run --rm -v app-data:/var/lib/app example-app:1.0
# Replace the image default command
docker run --rm example-app:1.0 ./app --check
# Diagnose an image containing a shell
docker run --rm --entrypoint /bin/sh -it example-app:1.0

-p hostPort:containerPort publishes a port; EXPOSE alone does not. A service must listen on an interface reachable from the container network, not only on loopback. Inspect the result with:

docker image inspect example-app:1.0
docker history example-app:1.0

Inspection reveals the configured user, environment, entrypoint, command, and exposed ports. History helps identify large layers, unexpected commands, and possible sensitive data.

Layer caching and efficient builds

Image layers record filesystem changes and configuration results. A build cache reuses a previous result when the instruction and relevant inputs still match. A changed file invalidates the related COPY cache and usually invalidates following instructions.

FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

Copy stable dependency manifests before frequently changing application source. Source edits can then reuse the expensive dependency-install layer. Combine tightly related package operations, such as package index refresh, installation, and metadata cleanup, in one readable RUN instruction so stale package indexes and caches do not remain in an earlier layer.

BuildKit supports cache mounts for package managers. The cache is reusable build data, not runtime image content:

# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-cache-dir -r requirements.txt

Use docker build --no-cache when diagnosing stale results or intentionally rebuilding every step. Cache invalidation should otherwise be deliberate, such as changing a version argument or dependency lockfile.

Image size and production optimization

  • Choose a minimal base appropriate for the application, balancing size against compatibility, debugging tools, certificates, locale data, and security support.
  • Exclude unnecessary files with .dockerignore.
  • Remove package indexes and temporary artifacts in the same layer that creates them.
  • Install dependencies from lockfiles or other explicit, reproducible specifications.
  • Use multi-stage builds to keep compilers, test tools, source code, and development dependencies out of the runtime image.
  • Do not remove runtime libraries, CA certificates, timezone data, user files, or other assets the application actually needs.

Multi-stage builds

A multi-stage build uses multiple FROM instructions. Named stages separate compilation from execution, and COPY --from transfers only selected artifacts.

FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server

FROM gcr.io/distroless/static-debian12 AS runtime
WORKDIR /app
COPY --from=build /out/server /app/server
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/app/server"]

The builder contains the compiler and source tree; the runtime stage contains only the executable and required runtime assets. Build a named stage for development or diagnostics:

docker build --target build -t example-app:build .

Shared base stages can provide common dependencies for test and production stages. Compare stages before choosing an architecture:

ConcernSingle-stage buildMulti-stage build
Final image sizeOften includes build materialCan contain only runtime artifacts
Build tool exposureUsually remains installedConfined to the builder stage
Runtime dependenciesMixed with development dependenciesSelected explicitly
ComplexitySimpler initiallyMore stage design and testing
Production suitabilitySuitable for simple applicationsUsually preferable for compiled applications

Security and supply-chain practices

  • Use trusted, maintained base images and pin important versions or digests.
  • Never copy credentials, tokens, passwords, private keys, or local secret files into the context.
  • Do not put secrets in ENV or ARG. Deleting a secret later does not remove it from an earlier layer.
  • Use BuildKit secret mounts or SSH forwarding for temporary build-only access.
  • Run the application as an unprivileged user and set ownership explicitly.
  • Minimize installed packages and remove build tooling from production stages.
  • Scan images, generate or retain provenance information in the build pipeline, and update base images regularly.
# Dockerfile syntax enabling BuildKit features
# syntax=docker/dockerfile:1
FROM alpine:3.20
RUN --mount=type=secret,id=private_token \
    TOKEN_FILE=/run/secrets/private_token \
    ./install-private-dependency.sh
docker build --secret id=private_token,src=./private-token -t example-app:1.0 .

The secret is mounted only for the RUN instruction. Avoid COPY and ENV for credentials, and verify history and scanning results after the build.

Application patterns

Static web application

FROM node:22-slim AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /src/dist /usr/share/nginx/html
EXPOSE 80

Interpreted web service

FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app . .
RUN useradd --system --create-home app
USER app
EXPOSE 8080
CMD ["python", "-m", "myapp"]

In a real image, create the user before using it for ownership, or use a base image that already provides the user. Development images may add reload tools and debug dependencies, while development runs commonly use bind mounts such as -v "$PWD:/app". Production images should not assume those host files or tools exist.

Compiled application

The Go example above demonstrates the standard pattern: copy manifests, download dependencies, compile in a named builder stage, and copy the resulting executable into a minimal runtime stage. Confirm that the binary's required shared libraries, certificates, and user files exist before choosing an extremely minimal runtime base.

Debugging and validation

Read build output from the first failing instruction. Check the context, paths, package names, architecture, and command exit status. For a failing startup, inspect logs and configuration, then replace the entrypoint with a shell when the image provides one:

docker logs <container>
docker image inspect example-app:1.0
docker run --rm --entrypoint /bin/sh -it example-app:1.0
docker run --rm --entrypoint /app/server example-app:1.0 --help
SymptomLikely causeDiagnostic approachResolution
COPY cannot find a fileWrong context, ignored file, or misspelled context-relative pathConfirm the final build argument and inspect .dockerignoreCorrect the context, ignore rule, or source layout
Old dependencies remainCache reuse, poor manifest ordering, or an old tagRead build output, history, and image IDsOrder manifests first; use deliberate invalidation or --no-cache
Container exits immediatelyCommand finishes, startup fails, or configuration is missingRead logs and override the entrypointFix the command or provide required runtime configuration
Run arguments do not replace the commandENTRYPOINT appends arguments; shell form changes behaviorInspect entrypoint and commandUse exec form or intentionally pass --entrypoint
Port is unreachableLoopback binding, wrong mapping, or confusion about EXPOSECheck application bind address and -pListen on the appropriate interface and publish the correct port
Permission denied after USERRoot-owned files or unwritable mounted directoryInspect ownership inside a diagnostic shellUse --chown, create writable directories, and account for mount ownership
Image is too largeLarge context, broad base, build tools, or cleanup in a later layerUse docker history and review context contentsImprove .dockerignore, clean in one layer, and use multi-stage builds
Architecture or library failureImage platform mismatch or missing shared librariesInspect image platform, executable format, and library dependenciesBuild for the target platform and select a compatible runtime base
Secret appears in imageCredential copied, passed through ARG/ENV, or deleted after a layerInspect history and scanner resultsRotate it, rebuild from a clean context, and use secret mounts or runtime injection

Validate startup, logs, port connectivity, health state, filesystem permissions, and graceful shutdown. A HEALTHCHECK reports starting, healthy, or unhealthy based on repeated command results. Test that the check reflects actual service readiness and that the selected STOPSIGNAL lets the application shut down cleanly.

Maintainability checklist

  • Use a predictable order: parser directives, global arguments, base stage, labels, environment, working directory, dependency manifests, installation, source, user, ports, health check, and startup configuration.
  • Name stages clearly, such as build, test, and runtime.
  • Comment non-obvious decisions rather than restating each instruction.
  • Document build arguments, runtime variables, ports, volumes, and expected commands.
  • Keep Dockerfiles in version control and test builds and startup behavior in CI.
  • Review image history, scans, provenance, and base-image updates as part of maintenance.

Related concepts

After mastering Dockerfiles, study Dockerfile image construction alongside Docker Compose, registries and tagging, networking and port publishing, volumes, image scanning, CI/CD builds, Kubernetes image requirements, OCI images, and multi-platform Buildx builds.