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:
- Write a Dockerfile and supporting files.
- Build an image from a context.
- Inspect its history and configuration.
- Run a container from the image.
- 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
| Instruction | Primary purpose | Build-time or runtime effect | Key cautions | Typical example use |
|---|---|---|---|---|
FROM | Select a base image | Starts a stage | Prefer maintained tags or digests; scratch has no shell or libraries | FROM python:3.12-slim |
RUN | Execute build commands | Creates a layer | Clean temporary data in the same step | Install packages or compile code |
COPY | Copy context files or stage artifacts | Creates a layer | Paths are relative to context; use ownership options where supported | COPY --chown=app:app . . |
ADD | Copy files with extra behavior | Creates a layer | Can extract local archives and retrieve remote sources; behavior can surprise reviewers | Deliberate archive extraction |
WORKDIR | Set the working directory | Changes later build and default runtime directory | Prefer it to repeated cd commands | WORKDIR /app |
ENV | Set environment variables | Persists in image configuration and runtime | Do not use for secrets | ENV APP_ENV=production |
ARG | Accept build parameters | Available during build only unless copied into ENV | Values can appear in build metadata; not secret storage | ARG VERSION=1.0 |
EXPOSE | Document intended ports | Image metadata only | Does not publish a host port | EXPOSE 8080 |
USER | Select the build and runtime user | Affects later instructions and default process | Ensure files and writable directories have suitable ownership | USER app |
CMD | Set default command or arguments | Runtime configuration | Only the last CMD is effective; easily overridden | CMD ["./server"] |
ENTRYPOINT | Set the primary executable | Runtime configuration | Only the last one is effective; shell form handles signals poorly | ENTRYPOINT ["./server"] |
LABEL | Attach metadata | Image configuration | Keep values useful and non-sensitive | Source, version, license |
VOLUME | Declare a mount point | Runtime and image metadata | Data behavior changes when a volume is mounted | VOLUME ["/var/lib/app"] |
SHELL | Change shell for shell-form instructions | Build behavior | Use only when shell semantics require it | SHELL ["/bin/bash", "-c"] |
HEALTHCHECK | Report service health | Runtime health state | A check must test real readiness, not merely process existence | HEALTHCHECK CMD curl -f http://localhost:8080/health |
STOPSIGNAL | Choose the stop signal | Runtime behavior | Use a signal the application handles correctly | STOPSIGNAL SIGTERM |
ONBUILD | Defer an instruction to child images | Descendant build behavior | Use deliberately because effects are invisible until inheritance | ONBUILD 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.
| Characteristic | ARG | ENV |
|---|---|---|
| Available during build | Yes, after declaration | Yes, after declaration |
| Available in a running container | Not automatically | Yes |
| Persistence in image configuration | Not as runtime environment, but values may appear in metadata or history | Yes |
| Suitable for secrets | No | No |
| Scope behavior | Stage-scoped; pre-FROM arguments need redeclaration in a stage | Applies from its declaration onward |
| Override mechanism | docker build --build-arg NAME=value | docker 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 form | CMD form | Resulting startup behavior | How docker run arguments affect it | Recommended use case |
|---|---|---|---|---|
| Exec | Exec | Entrypoint executable receives CMD arguments | Arguments replace CMD arguments and are appended to entrypoint | Most services and CLIs |
| Exec | Shell | CMD becomes a single default argument string in less predictable combinations | Usually avoid for direct argument composition | Rare, deliberate wrapper behavior |
| Shell | Any | Shell becomes the effective process | Arguments generally do not receive the intended direct composition | Commands requiring shell syntax |
| None | Exec or shell | CMD is the default executable and arguments | Runtime arguments replace CMD | Simple 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:
| Concern | Single-stage build | Multi-stage build |
|---|---|---|
| Final image size | Often includes build material | Can contain only runtime artifacts |
| Build tool exposure | Usually remains installed | Confined to the builder stage |
| Runtime dependencies | Mixed with development dependencies | Selected explicitly |
| Complexity | Simpler initially | More stage design and testing |
| Production suitability | Suitable for simple applications | Usually 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
ENVorARG. 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
| Symptom | Likely cause | Diagnostic approach | Resolution |
|---|---|---|---|
COPY cannot find a file | Wrong context, ignored file, or misspelled context-relative path | Confirm the final build argument and inspect .dockerignore | Correct the context, ignore rule, or source layout |
| Old dependencies remain | Cache reuse, poor manifest ordering, or an old tag | Read build output, history, and image IDs | Order manifests first; use deliberate invalidation or --no-cache |
| Container exits immediately | Command finishes, startup fails, or configuration is missing | Read logs and override the entrypoint | Fix the command or provide required runtime configuration |
| Run arguments do not replace the command | ENTRYPOINT appends arguments; shell form changes behavior | Inspect entrypoint and command | Use exec form or intentionally pass --entrypoint |
| Port is unreachable | Loopback binding, wrong mapping, or confusion about EXPOSE | Check application bind address and -p | Listen on the appropriate interface and publish the correct port |
Permission denied after USER | Root-owned files or unwritable mounted directory | Inspect ownership inside a diagnostic shell | Use --chown, create writable directories, and account for mount ownership |
| Image is too large | Large context, broad base, build tools, or cleanup in a later layer | Use docker history and review context contents | Improve .dockerignore, clean in one layer, and use multi-stage builds |
| Architecture or library failure | Image platform mismatch or missing shared libraries | Inspect image platform, executable format, and library dependencies | Build for the target platform and select a compatible runtime base |
| Secret appears in image | Credential copied, passed through ARG/ENV, or deleted after a layer | Inspect history and scanner results | Rotate 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, andruntime. - 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.