Bigger .dockerignore, Smaller Docker Images
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.