Bigger .dockerignore, Smaller Docker Images

Starlight Romero
Nerd For Tech
Published in
6 min readJul 1, 2021

--

This is the first article in the four part series, Minimizing & Securing Docker Images. Check out the other articles in the series:
1.
Bigger .dockerignore, Smaller Docker Images
2.
Look Docker, No Distro

Everyone wants faster build times, and less junk in their app. By beefing up our .dockerignore we can make smaller Docker images. The benefits of smaller images don’t stop at faster build times. Smaller images take up less disk space which really starts to shows off some benefits when the application is scaled up, potentially in an auto-scaling Kubernetes cluster. Lastly, smaller Docker images have less attack surface.

This is the first part in a four part series focusing on optimizing Docker image size and security. This article has a corresponding repository which serves to showcase a real world example. The repo can be found here. Each branch will build off of the previous showcasing the different stages we will go through in the article.

🔤 Back to the Basics

Much like a .gitignore file which defines the files that we want git to ignore, a .dockerignore file defines the files that we want Docker to ignore. But why do we want Docker to ignore certain files? Going back to wanting smaller images, a smaller image means faster build times when we run docker run or docker-compose up. If a file isn’t needed for your app to run, put it in the .dockerignore. Now that this file is in the .dockerignore, it won’t be included in the Docker image, reducing the size of your image. The Docker CLI looks for .dockerignore in the root of your app. If it is not in the root folder, it will not be read.

Some examples of files to put in your .gitignore are:

  • .git
  • .vscode
  • .gitignore
  • build
  • dist
  • node_modules
  • Makefile
  • README.md

Branch 01-Basics is the corresponding branch to this section. Cloning and running the app starts a server at port 8080. Navigating to localhost:8080, we can see the text, “Hello, World! The secret is 1234”.

Diving into the .dockerignore file, we can see the files we are ignoring:

On step 8/9 of the Dockerfile we run a ls -la command. We can see the output below:

This is pretty good but what else can we get rid of?

🐳 Docker Stuff

Can we put Dockerfile or docker-compose.yml in the .dockerignore file? Yes! Throw those in there as well. If our app doesn’t directly need the file to run, it belongs in the .dockerignore. We can even put .dockerignore itself into the .dockerignore file! Why is this?

Yes, the Dockerfile, docker-compose.yml, and .dockerignore are all used to build the image and spin up a container, however, that’s where the purpose of these files stop. They are not used in our app. If we don’t need the files to run the app without Docker, we do not need the files to run the app with Docker. We need the files to build the Docker image and we need the files to run the Docker container. Yet, the app within the Docker container does not need these files.

🔒 Environment Variables

Environment files always confused me in the context of Docker. Unlike Dockerfile, the app does need .env in order to run properly, both when using Docker and without using Docker. However, if we are using a docker-compose.yml file, like we are in this app, and in the yml file we define an env_file, then the .env file can be ignored.

Defining an env_file in docker-compose.yml is the declarative version of docker-compose --env-file ./.env. Both approaches define the path to an environment file containing secret variables, allowing docker-compose to access and pass along the variables to the app running inside the container. By passing the environment file to docker-compose, Docker itself doesn’t need to know about the environment file. This is because docker-compose acts like a wrapper around the Dockerfile. Docker-compose is the declarative version of docker run …. Docker-compose starts the Dockerfile build and is able to pass along the environment file to the Docker build without the Dockerfile directly needing .env.

🧪 Route and Unit Tests

Tests aren’t needed to run the app, but they are needed to test the app. One option is to test the app outside of a Docker container. In this scenario, we can ignore the test files. This path has the downside of any application outside of Docker, the environment where the app runs is not guaranteed to have all the correct packages, package versions, configurations, etc.

Testing your application inside of a Docker container provides more consistency in a standardized environment. How can we test the app when the test file is being ignored? We will circle back to this in the Hot Reloading section below. However, for now, we will ignore the test file.

🐗 Wildcard Selectors

In this section we won’t be adding any additional files to the .dockerignore. We will be condensing the amount of lines in our .dockerignore by using wildcard selectors. The * allows us to fill an indefinite amount of characters. On line 5, we have docker-compose*.yml. This will ignore docker-compose.yml, docker-compose.dev.yml , and docker-composeyou-can-put-anything-here.yml. We have similar syntax on line 3 to ignore both .dockerignore and .gitignore. Lines 6 and line 9 follow the same logic.

Line 7 follows a different pattern, **. The double wildcard allows us to match any number of directories (including zero). Therefore **/*_test.go will match any test file in any directory. This one little, powerful line has all the power defined below (and more):

  • ./main_test.go
  • /tests/main_test.go
  • /tests/auth_test.go
  • /test/auth/main_test.go
  • /test/auth/login_test.go
  • ./mainTest.go
  • /tests/authTest.go

The ls -la command in our Dockerfile produces the same output as the section before:

One command which is good to know but I have never found a need to use is the !. By putting a ! in front of any line in the .dockerignore, Docker will ignore ignoring it. What a mouthful! Basically, Docker will make sure to include the file in the image. Here is one non-practical example:

*.md
!README.md

We are ignoring all markdown files except for README.md.

🔃 The Pros and Cons of Hot Reloading

Bind mounts are an extremely useful type of volume that docker-compose allows us to use. They essentially “bind” a file from outside the Docker container to a file inside the container. This allows for hot reloading. However, I often see them misused. Truth be told, I also didn’t know the best way to use them. At first, I would bind everything outside the container to everything inside the container with a volume .:/app or .:/usr/src/app.

The problem with this approach can be better understood if we understand the order of operations. When we run docker-compose up or any variation of the command, first the docker-compose command is converted into a docker command. Next, the .dockerignore is read and any files in there are ignored. After that, the Dockerfile is read. Of course when we do COPY . . we only copy the files that Docker did not ignore. Then, any volumes are applied, including bind mounts. The volumes do not adhere to the .dockerignore. Finally, if there is a command specified in the docker-compose.yml, that command is run.

By making a bind mount .:/app, we are completely overriding the .dockerignore. Since the bind mount it attached after the image is build, even files that were ignored are now mounted to the container. A better way to use bind mounts is to map specific files or folders. The following is an example of a development docker-compose.dev.yml where we set a bind mount only on the main.go file and map it to the file within the container at the WORKDIR location /app/main.go.

Going back to tests we can apply our new knowledge. Due to bind mounts being applied after the image builds, even if we ignore the tests we can make them accessible in the container again by adding a volume ./main_test.go:/app/main_test.go. In this docker-compose we can see the go test command is run. Due to docker-compose commands being executed after the volumes are mapped, we can be assured that the main_test.go file will be available inside of the container by the time go test is run.

📦 Packing It All Up

Small image sizes not only reduce build time and disk space, they also decrease our attack surface. The .dockerignore file is just the first piece in beginning to optimize Docker image size. You should now have a solid understanding of .dockerignore, how to use it, and how it works in relation to other Docker components. Now, try these tactics out in your next project and see how much you can reduce your image size.

--

--