Build a Reproducible Docker Image from a Dockerfile
A Dockerfile turns a folder of code into a tagged Docker image, and a careless one turns your whole project directory into the build context too. In about 15 minutes you will write one, build and tag an image, run it, and check what actually went in.
The route
Jump straight to the step you need, or tick off Done means at the end.
The examples use Docker 29.8.1, supplied here by docker-ce-cli. You need a working Docker engine, the docker command, and a shell. Building an image normally needs access to the Docker daemon; if your account is not allowed to use it, use your site's approved Docker access method or an authorised sudo command.
1. Choose a clean build directory
Create a directory containing only the files intended for the image, then enter it:
mkdir docker-demo
cd docker-demo
cat > server.sh <<'EOF'
#!/bin/sh
printf 'hello from the image\n'
EOF
chmod 755 server.sh
The final argument to docker build is the build context. With ., the whole current directory becomes available to build instructions such as COPY. That makes the directory boundary an easy place to slip up: credentials, build artefacts and large dependency trees can all be sent to the builder unless you exclude them.
2. Exclude accidental input
Create a .dockerignore before building. These patterns are examples; adjust them to the files your project actually uses:
cat > .dockerignore <<'EOF'
.git
.env
*.log
node_modules
EOF
A Dockerfile-specific ignore file takes precedence over the context's root .dockerignore. Docker still needs the Dockerfile and ignore file themselves to perform the build, but an ignore rule is not a mechanism for copying those files into the image. Treat ignore patterns as a reviewable security boundary, not merely a speed optimisation.
Checkpoint
Run find . -maxdepth 2 -type f -print and confirm that no private key, token or production configuration is in the context. The command is read-only.
3. Write the Dockerfile
FROMstarts a build stage.WORKDIRmakes later paths predictable.COPYimports a file from the context.RUNexecutes during the build.CMDsupplies the default command when a container starts.
cat > Dockerfile <<'EOF'
FROM alpine:3.20
WORKDIR /app
COPY server.sh ./server.sh
RUN /bin/sh -n ./server.sh
CMD ["/app/server.sh"]
EOF
The exec form of RUN and CMD is a JSON array, so it needs double quotes. The RUN check runs during the build and creates a layer; CMD does not run until a container starts. That distinction prevents a common debugging mistake: changing CMD does not test the command at build time.
Pinning a base image tag is clearer than silently accepting an unexamined default, but a tag can still move. For a release build, record and review a digest, for example alpine:3.20@sha256:REPLACE_WITH_A_REVIEWED_DIGEST. Do not paste an unverified digest into production.
4. Build and tag the image
Run this as your ordinary account. Add sudo only when that is the established way your machine grants Docker daemon access:
docker build --tag docker-demo:local .
Successful output ends with an image export or naming line similar to:
Successfully tagged docker-demo:local
The exact progress format depends on the builder, so use the exit status as the first check. The context shown in the output should be small enough to match the files you intended to send.
If a build fails with a missing file error, check whether the source is outside the context or excluded by .dockerignore. If a command fails in RUN, reproduce that command in a temporary container based on the same base image rather than weakening the Dockerfile blindly.
5. Run and verify the result
Start a short-lived container. This changes Docker daemon state only while the container exists:
docker run --rm docker-demo:local
Expected output:
hello from the image
--rm removes the stopped container automatically. If you omitted it and need to clean up a stopped test container, list it with docker ps --all, then remove the specific ID with docker rm CONTAINER_ID. Check the ID before removing anything: docker rm is destructive for that container's writable layer.
Inspect the image's configuration and size:
docker image inspect docker-demo:local
docker image ls docker-demo:local
Look for the configured working directory, entry command and image ID in the first command; the second should show the local tag and a size. These checks do not prove an application is healthy, but they confirm the intended Dockerfile metadata reached the image.
6. Handle values and secrets deliberately
Use ARG for a build-time value and ENV for a value that should persist into containers:
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
Build with docker build --build-arg APP_VERSION=1.2.3 --tag docker-demo:1.2.3 .. The ARG is not automatically present in the final container, while the ENV value is; an ENV with the same name overrides the argument from its point of definition.
Security boundary
Do not put passwords, private keys or API tokens in ARG, ENV, the Dockerfile, or a copied file. Build arguments can be exposed by image history and build provenance, and environment values remain in image configuration. Use the builder's secret-mount facility for a secret that must be available during a build, then verify the resulting layers contain no secret.
Common traps
- Wrong context:
docker build -f path/to/Dockerfile .uses the current directory as context, not the Dockerfile's directory. Pass the intended context explicitly. - Shell versus exec form:
RUN echo $VALUEis interpreted by a shell, whileRUN ["echo", "$VALUE"]does not invoke one automatically. Use the form that matches what the command needs. - Unexpected cache: use
docker build --no-cache --tag docker-demo:clean .when investigating stale build results. Add--pullas well when you deliberately want to check for a newer base image; this can download and rebuild substantially more, so do not use it for every local edit. - Too much input: refine
.dockerignoreand rebuild. Never solve a context problem by copying an entire home directory or repository into the image.
Done means
- Dockerfile reviewed.
Dockerfilestarts with a reviewedFROMand uses an explicitWORKDIR. - Ignore file set.
.dockerignoreexcludes credentials, VCS data and irrelevant large files. - Build succeeded.
docker build --tag docker-demo:local .exits successfully. - Run verified.
docker run --rm docker-demo:localprints the expected result. - Secrets checked. You inspected the image and confirmed no secret was supplied through the build context,
ARGorENV.