
Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: July 21, 2025
The Docker suite is a staple for container application development, delivery, and deployment. Within its flexible structure, the Docker system provides many ways to define and configure containers. One of the main ways to ensure a Docker application has a consistent behavior is to use a Dockerfile when building its container image. However, the syntax and options of this feature have been evolving.
In this tutorial, we look at a specific Dockerfile option and how advanced features might cause problems depending on the setup and environment. First, we briefly refresh our knowledge about the Dockerfile concept. After that, we go over the evolution of its syntax. Next, we delve into ways to introduce build-time data within the container image. Finally, we talk about feature support inside a Dockerfile and during a build.
We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. Unless otherwise specified, it should work in most POSIX-compliant environments.
Before delving into the specifics, let’s briefly discuss the idea, structure, and mechanics behind a Dockerfile, one of the main ways to create and define container images.
A Dockerfile is a regular text file with the recipe for a Docker container image. Usually, this means we set the so-called base image file, on top of which we make customizations:
For clarity and reference, let’s define a minimalistic Dockerfile:
FROM debian:latest
CMD ["bash"]
Here, we base the new image on the latest version of the debian image. After that, we define the initial command to run when starting the container as bash.
Of course, there are many other options, configurations, and possibilities to choose from when writing a Dockerfile. Yet, only some of them are more common and standardized.
Earlier versions of a Dockerfile only supported basic instructions such as the aforementioned FROM and CMD, along with some others:
Naturally, these are only some examples of the foundation that Dockerfile started with.
Later, healthcheck options and multi-stage builds drastically improved the ease with which developers could ensure the stability of containers.
Notably, BuildKit was introduced as a faster, feature-rich, user-friendly, and overall better engine to perform builds based on a Dockerfile. One of its features is a way to specify build-time mounts.
Docker mounts are a way to enable access to data outside the Docker image itself.
Notably, Docker mounts via the runtime –volume (-v) flag of the docker build command aren’t supported by the Dockerfile syntax.
Essentially, a Docker application developer can link dynamic data sources from any outside environment. On the other hand, users and deployment teams may change the contents of data sources without going through the image or its components.
Further, since Docker images are ephemeral, e.g., they get reset on each restart, Dockerfile mounts are also a way to provide persistence.
To employ Dockerfile mounts, we use the RUN command with the –mount option:
RUN --mount=[type=TYPE],option=<value>[...]
This way, we can create a special mount type with particular options.
The default mount type is bind. However, different mount types are available to support special cases.
Let’s list supported Dockerfile mount types:
Now, let’s see the –mount option in action.
Let’s assume we want to mount a given local file or directory:
# syntax=docker/dockerfile:1.4
FROM debian:latest
CMD ["bash"]
RUN --mount=type=bind,source=/tmp/file666,target=/file1
In this case, we create a basic bind mount from the local file /tmp/file666 to the path /file1 within the container image.
Now, we can move on to more complex scenarios.
So, let’s look at type=cache:
# syntax=docker/dockerfile:1.4
FROM node:20-slim
WORKDIR /xpp
COPY package*.json ./
RUN --mount=type=cache,target=/root/npmcache
npm ci
COPY . .
CMD ["node", "main.js"]
In this case, we base the image on the slim modification of node version 20. After setting the WORKDIR working directory, we COPY the package*.json files responsible for the dependencies. Then, the RUN command uses /root/npmcache in the container as the cache, while the actual files are written to a location outside the container, managed by Docker.
Often, container image builds require credentials. In similar cases, the type=secret mount can be indispensable.
For instance, let’s create a secret mount in a new Dockerfile:
# syntax=docker/dockerfile:1.4
FROM debian:latest
RUN --mount=type=secret,id=xecret
sh -c 'read xecret < /run/secrets/xecret; echo "The secret is: $xecret"'
Here, we just add the xecret secret, which becomes automatically accessible within /run/secrets/ and only exists during the build instead of being available in the image afterward.
After that, we use the –secret parameters with the specific id and src to populate the secret at build time:
$ DOCKER_BUILDKIT=1 docker build
--secret id=xecret,src=xecret.txt
-t seccon .
Thus, xecret.txt becomes the file that /run/secrets/xecret points to while building, passing information without risk.
When building images, access to protected external sources is often required to make a container actually useful and portable.
For instance, application code is rarely stored locally and might be hosted within a repository or even a basic SSH server:
# syntax=docker/dockerfile:1.4
FROM debian:latest
RUN apt-get update && apt-get install -y git openssh-client
RUN --mount=type=ssh
git clone [email protected]:example-user/example-repo.git /xpp
In this Dockerfile, we use git to clone a repository after adding a type=ssh mount. Notably, we use the local package manager to ensure the git and openssh-client packages are in place.
At this point, let’s ensure the SSH agent is running and the proper key is added:
$ eval "$(ssh-agent -s)"
Agent pid 66600
$ ssh-add $HOME/.ssh/id_rsa
Once we have the proper build recipe and SSH agent, the –ssh command-line parameter of docker build should be all we need during the build process:
$ docker build --ssh default -t sshcon .
In this case, we employ the default agent setup, but we could use another name or even a custom socket path.
Notably, in each instance, depending on the context, we used one of two special additions:
Let’s see what each one means and other alternatives we can leverage.
The # syntax line is a kind of shebang or preprocessing instruction within a Dockerfile:
# syntax=docker/dockerfile:1.4
It tells the build engine to allow and employ the features of a particular syntax version. Without a syntax specifier, we might encounter an error when using more advanced constructs, since the default version is commonly 1.0:
Error response from daemon: Dockerfile parse error line #: Unknown flag: mount
Of course, this line only pertains to and is mandatory for the Dockerfile.
There are other means to instruct Docker which build engine to use and enable the option.
Similarly, we might get an error if we don’t set the DOCKER_BUILDKIT environment variable to 1 before a build. Exported or just prepended to the line, Docker references the variable to know whether it should use the BuildKit (non-0) instead of the classic build engine (0).
Yet, there is another way to indicate that without runtime specifications.
Lastly, we can modify the config.json Docker configuration file to set BuildKit as the permanent build engine:
$ cat $HOME/.docker/config.json
[...]
{
"features": {
"buildkit": true
}
}
[...]
This way, we don’t need to specify DOCKER_BUILDKIT=1 in the environment. However, we still need to add the syntax specifier in each Dockerfile.
In this article, we understood what a Dockerfile is, how it evolved, and ways to use special mount types. Furthermore, we saw how special build features can be employed.
In conclusion, although there are alternatives, Dockerfile is still one of the main ways to build images, even when builds require advanced functionality.