1. Overview

In general, the Docker build command restricts the sources of files we can use in our Docker images. We specify a build context, which is the root from which both the Dockerfile and all its dependent files must be found.

However, sometimes, we might wish to use a Dockerfile from one part of our filesystem with files from another.

In this short tutorial, we’ll look at a few ways to overcome this limitation, along with their pros and cons.

2. The Docker Build and Its Context

2.1. A Conventional Docker Build

Let’s start with an example – a simple nginx application that has one HTML file and a Dockerfile. The directory structure is:

projects
├ <some other projects>...
└── sample-site
       ├── html
       │    └── index.html
       └── Dockerfile

We also have a small Dockerfile:

FROM nginx:latest
COPY html/* /etc/nginx/html/

Now, to build an image for this application, let’s run:

$ docker build -t sample-site:latest .

Here, the build context is set to the current directory via the “.” argument.

It’s a common practice to keep the Dockerfile at the project root directory. The command, by default, expects the Dockerfile to be present there. All the files we want to include in the image should exist somewhere inside that context.

2.2. More Complex Build Scenarios

Sometimes the conventional approach may not work for us.

For example, we may have different Docker or docker-compose files based on the environment. Or, we may be adding our containers as a separate activity from our development.

In these situations, it makes sense to move the Docker-related files to a separate directory. Similarly, on some occasions, we might keep configuration files for our images outside of our project root directory.

Unfortunately, Docker prevents us from adding files from arbitrary parts of the file system as this might open up a security hole. However, there are a few workarounds:

  • Build with a bigger context to include everything needed
  • Create a base image with the files residing outside the context and then extend the base image
  • Copy all the necessary files to create a temporary context and build the image from it

Let’s check them out one-by-one.

3. Build With a Bigger Context

Let’s assume we want to move the Dockerfile into a separate directory named docker. We also want to override the standard nginx config with a custom config file that’s in the config directory outside the project root sample-site. The new directory structure is:

projects
├ <some other projects>...
├── sample-site
│      ├── html
│      │    └── index.html
│      └── docker
│           └── Dockerfile
└── config
      └── nginx.conf

In this case, neither the earlier Dockerfile nor the docker build command works anymore. To make it work again, we have to build it with a bigger context – the projects directory.

3.1. Change the Dockerfile

Now that our context has changed, we need to change our Dockerfile:

FROM nginx:latest
COPY sample-site/html/* /etc/nginx/html/
COPY config/nginx.conf /etc/nginx/nginx.conf

We’ve modified the path of the html directory with respect to the new context. We’ve also included the nginx.conf from its location.

3.2. Build the Image

Now, let’s go to the projects directory and run the command to build the image:

$ cd projects
$ docker build -f sample-site/docker/Dockerfile -t sample-site:latest .

Here again, we use “.” as the context, as we’re running the command from the projects directory. This brings both the Dockerfile and nginx.conf inside the current build context. As the Dockerfile isn’t in the root of the context directory, we provide its path using the -f option.

The problem with this approach is that the Docker client sends a copy of the build context – the whole projects directory – to the Docker daemon. The directory may contain many other unrelated files and directories. So, this may require Docker to scan a lot of resources, which can cause the build process to be slow.

4. Create a Base Image With the External Files

Another approach is to create a base image with the external files and extend it afterward. We’ll reuse the same structure as in the previous example:

projects
├ <some other projects>...
├── sample-site
│      ├── html
│      │    └── index.html
│      └── docker
│           └── Dockerfile
└── config
       └── nginx.conf

4.1. Write Dockerfile for the Base

First, let’s write a Dockerfile with the config:

FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf

We place the file into the projects/config directory.

4.2. Build the Base

The next step is to run the build command in projects/config to create the base image:

$ docker build -t sample-site-base:latest .

Now, we have a Docker image called sample-site-base:latest, containing the nginx server and the configuration files.

4.3. Write Dockerfile for the Child

Next, let’s write the Dockerfile of the sample-site to extend sample-site-base:

FROM sample-site-base:latest
COPY html/* /etc/nginx/html/

4.4. Build the Child

Finally, let’s run the command in projects/sample-site to build the image of our application:

$ docker build -f docker/Dockerfile -t sample-site:latest .

Here, we’ve split the Docker build into two separate stages, each relating to a different directory tree in our project.

This approach reuses the common part of a Dockerfile across its child images. This structure is relatively easy to maintain. If there’s any change in the base image, we need to rebuild the child images, and the same change reflects in all of them.

We should note that this approach increases the number of layers in our final Docker image.

5. Create a Temporary Context

Our final option is to create a tailor-made temporary context to build the image. This uses some scripting around our docker build command to pull the necessary files into a Docker-friendly directory structure.

5.1. Create a Temporary Directory

First, let’s create a temporary directory and copy all the necessary assets:

$ mkdir tmp-context
$ cp -R ../html tmp-context/
$ cp -R ../../config tmp-context/

This will be our build context.

5.2. Create the Dockerfile

Now, let’s write the Dockerfile relative to this context:

FROM nginx:latest
COPY html/* /etc/nginx/html/
COPY config/nginx.conf /etc/nginx/nginx.conf

We’ve already put the required files in tmp-context, so we don’t need to mention any outside path here.

5.3. Build the Image

Let’s run the command to build the image:

$ cd tmp-context
$ docker build -t sample-site:latest .

This command takes tmp-context as the build context. It finds everything it needs inside the directory and, thus, builds the image without any problem.

5.4. Clean Up

Finally, let’s clean up the temporary directory:

$ rm -rf tmp-context

This is the cleanest way to add files from anywhere to the Docker image. Here, we have full control over how we create our context. We may create a bash script with all the above commands to simplify the entire process.

However, we should understand that some files might be large. They could take a long time to get copied into the context, which would happen on every build.

6. Conclusion

In this article, we saw how Docker usually expects to build images from files in the same directory as the Dockerfile. We looked at some solutions for adding files from outside the usual build context.

We also explored the advantages and the disadvantages of each solution.

As always, the sample code related to this article is available over on GitHub.