Optimize Windows Dockerfiles
There are many ways to optimize both the Docker build process and the resulting Docker images. This article explains how the Docker build process works and how to optimally create images for Windows containers.
Image layers in Docker build
Before you can optimize your Docker build, you'll need to know how Docker build works. During the Docker build process, a Dockerfile is consumed, and each actionable instruction is run, one-by-one, in its own temporary container. The result is a new image layer for each actionable instruction.
For example, the following sample Dockerfile uses the mcr.microsoft.com/windows/servercore:ltsc2019
base OS image, installs IIS, and then creates a simple website.
# Sample Dockerfile
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN dism /online /enable-feature /all /featurename:iis-webserver /NoRestart
RUN echo "Hello World - Dockerfile" > c:\inetpub\wwwroot\index.html
CMD [ "cmd" ]
You might expect that this Dockerfile will produce an image with two layers, one for the container OS image, and a second that includes IIS and the website. However, the actual image has many layers, and each layer depends upon the one before it.
To make this clearer, let's run the docker history
command against the image our sample Dockerfile made.
docker history iis
IMAGE CREATED CREATED BY SIZE COMMENT
f4caf476e909 16 seconds ago cmd /S /C REM (nop) CMD ["cmd"] 41.84 kB
f0e017e5b088 21 seconds ago cmd /S /C echo "Hello World - Dockerfile" > c 6.816 MB
88438e174b7c About a minute ago cmd /S /C dism /online /enable-feature /all / 162.7 MB
6801d964fda5 4 months ago 0 B
The output shows us that this image has four layers: the base layer and three additional layers that are mapped to each instruction in the Dockerfile. The bottom layer (6801d964fda5
in this example) represents the base OS image. One layer up is the IIS installation. The next layer includes the new website, and so on.
Dockerfiles can be written to minimize image layers, optimize build performance, and optimize accessibility through readability. Ultimately, there are many ways to complete the same image build task. Understanding how the Dockerfile's format affects build time and the image it creates improves the automation experience.
Optimize image size
Depending on your space requirements, image size can be an important factor when building Docker container images. Container images are moved between registries and host, exported and imported, and ultimately consume space. This section will tell you how to minimize image size during the Docker build process for Windows containers.
For additional information about Dockerfile best practices, see Best practices for writing Dockerfiles on Docker.com.
Group related actions
Because each RUN
instruction creates a new layer in the container image, grouping actions into one RUN
instruction can reduce the number of layers in a Dockerfile. While minimizing layers may not affect image size much, grouping related actions can, which will be seen in subsequent examples.
In this section, we'll compare two example Dockerfiles that do the same things. However, one Dockerfile has one instruction per action, while the other had its related actions grouped together.
The following ungrouped example Dockerfile downloads Python for Windows, installs it, and removes the downloaded setup file once installation is done. In this Dockerfile, each action is given its own RUN
instruction.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN powershell.exe -Command Invoke-WebRequest "https://www.python.org/ftp/python/3.5.1/python-3.5.1.exe" -OutFile c:\python-3.5.1.exe
RUN powershell.exe -Command Start-Process c:\python-3.5.1.exe -ArgumentList '/quiet InstallAllUsers=1 PrependPath=1' -Wait
RUN powershell.exe -Command Remove-Item c:\python-3.5.1.exe -Force
The resulting image consists of three additional layers, one for each RUN
instruction.
docker history doc-example-1
IMAGE CREATED CREATED BY SIZE COMMENT
a395ca26777f 15 seconds ago cmd /S /C powershell.exe -Command Remove-Item 24.56 MB
6c137f466d28 28 seconds ago cmd /S /C powershell.exe -Command Start-Proce 178.6 MB
957147160e8d 3 minutes ago cmd /S /C powershell.exe -Command Invoke-WebR 125.7 MB
The second example is a Dockerfile that performs the exact same operation. However, all related actions have been grouped under a single RUN
instruction. Each step in the RUN
instruction is on a new line of the Dockerfile, while the '\' character is used to line wrap.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN powershell.exe -Command \
$ErrorActionPreference = 'Stop'; \
Invoke-WebRequest https://www.python.org/ftp/python/3.5.1/python-3.5.1.exe -OutFile c:\python-3.5.1.exe ; \
Start-Process c:\python-3.5.1.exe -ArgumentList '/quiet InstallAllUsers=1 PrependPath=1' -Wait ; \
Remove-Item c:\python-3.5.1.exe -Force
The resulting image has only one additional layer for the RUN
instruction.
docker history doc-example-2
IMAGE CREATED CREATED BY SIZE COMMENT
69e44f37c748 54 seconds ago cmd /S /C powershell.exe -Command $ErrorAct 216.3 MB
Remove excess files
If there's a file in your Dockerfile, such as an installer, that you don't need after it's been used, you can remove it to reduce image size. This needs to occur in the same step in which the file was copied into the image layer. Doing so prevents the file from persisting in a lower-level image layer.
In the following example Dockerfile, the Python package is downloaded, executed, then removed. This is all completed in one RUN
operation and results in a single image layer.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN powershell.exe -Command \
$ErrorActionPreference = 'Stop'; \
Invoke-WebRequest https://www.python.org/ftp/python/3.5.1/python-3.5.1.exe -OutFile c:\python-3.5.1.exe ; \
Start-Process c:\python-3.5.1.exe -ArgumentList '/quiet InstallAllUsers=1 PrependPath=1' -Wait ; \
Remove-Item c:\python-3.5.1.exe -Force
Optimize build speed
Multiple lines
You can split operations into multiple individual instructions to optimize Docker build speed. Multiple RUN
operations increase caching effectiveness because individual layers are created for each RUN
instruction. If an identical instruction was already run in a different Docker Build operation, this cached operation (image layer) is reused, resulting in decreased Docker build runtime.
In the following example, both Apache and the Visual Studio Redistribute packages are downloaded, installed, and then cleaned up by removing files that are no longer needed. This is all done with a single RUN
instruction. If any of these actions are updated, all actions will rerun.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN powershell -Command \
# Download software ; \
wget https://www.apachelounge.com/download/VC11/binaries/httpd-2.4.18-win32-VC11.zip -OutFile c:\apache.zip ; \
wget "https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe" -OutFile c:\vcredist.exe ; \
wget -Uri http://windows.php.net/downloads/releases/php-5.5.33-Win32-VC11-x86.zip -OutFile c:\php.zip ; \
# Install Software ; \
Expand-Archive -Path c:\php.zip -DestinationPath c:\php ; \
Expand-Archive -Path c:\apache.zip -DestinationPath c:\ ; \
Start-Process c:\vcredist.exe -ArgumentList '/quiet' -Wait ; \
# Remove unneeded files ; \
Remove-Item c:\apache.zip -Force; \
Remove-Item c:\vcredist.exe -Force; \
Remove-Item c:\php.zip
The resulting image has two layers, one for the base OS image, and one that contains all operations from the single RUN
instruction.
docker history doc-sample-1
IMAGE CREATED CREATED BY SIZE COMMENT
9bdf3a21fd41 8 minutes ago cmd /S /C powershell -Command Invoke-WebR 205.8 MB
6801d964fda5 5 months ago 0 B
By comparison, here are the same actions split into three RUN
instructions. In this case, each RUN
instruction is cached in a container image layer, and only those that have changed need to be rerun on subsequent Dockerfile builds.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN powershell -Command \
$ErrorActionPreference = 'Stop'; \
wget https://www.apachelounge.com/download/VC11/binaries/httpd-2.4.18-win32-VC11.zip -OutFile c:\apache.zip ; \
Expand-Archive -Path c:\apache.zip -DestinationPath c:\ ; \
Remove-Item c:\apache.zip -Force
RUN powershell -Command \
$ErrorActionPreference = 'Stop'; \
wget "https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe" -OutFile c:\vcredist.exe ; \
Start-Process c:\vcredist.exe -ArgumentList '/quiet' -Wait ; \
Remove-Item c:\vcredist.exe -Force
RUN powershell -Command \
$ErrorActionPreference = 'Stop'; \
wget http://windows.php.net/downloads/releases/php-5.5.33-Win32-VC11-x86.zip -OutFile c:\php.zip ; \
Expand-Archive -Path c:\php.zip -DestinationPath c:\php ; \
Remove-Item c:\php.zip -Force
The resulting image consists of four layers; one layer for the base OS image and each of the three RUN
instructions. Because each RUN
instruction ran in its own layer, any subsequent runs of this Dockerfile or identical set of instructions in a different Dockerfile will use cached image layers, reducing build time.
docker history doc-sample-2
IMAGE CREATED CREATED BY SIZE COMMENT
ddf43b1f3751 6 days ago cmd /S /C powershell -Command Sleep 2 ; Inv 127.2 MB
d43abb81204a 7 days ago cmd /S /C powershell -Command Sleep 2 ; Inv 66.46 MB
7a21073861a1 7 days ago cmd /S /C powershell -Command Sleep 2 ; Inv 115.8 MB
6801d964fda5 5 months ago
How you order the instructions is important when working with image caches, as you'll see in the next section.
Ordering instructions
A Dockerfile is processed from top to the bottom, each Instruction compared against cached layers. When an instruction is found without a cached layer, this instruction and all subsequent instructions are processed in new container image layers. Because of this, the order in which instructions are placed is important. Place instructions that will remain constant towards the top of the Dockerfile. Place instructions that may change towards the bottom of the Dockerfile. Doing so reduces the likelihood of negating existing cache.
The following examples show how Dockerfile instruction ordering can affect caching effectiveness. This simple example Dockerfile has four numbered folders.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN mkdir test-1
RUN mkdir test-2
RUN mkdir test-3
RUN mkdir test-4
The resulting image has five layers, one for the base OS image and each of the RUN
instructions.
docker history doc-sample-1
IMAGE CREATED CREATED BY SIZE COMMENT
afba1a3def0a 38 seconds ago cmd /S /C mkdir test-4 42.46 MB
86f1fe772d5c 49 seconds ago cmd /S /C mkdir test-3 42.35 MB
68fda53ce682 About a minute ago cmd /S /C mkdir test-2 6.745 MB
5e5aa8ba1bc2 About a minute ago cmd /S /C mkdir test-1 7.12 MB
6801d964fda5 5 months ago 0 B
This next Dockerfile has now been slightly modified, with the third RUN
instruction changed to a new file. When Docker build is run against this Dockerfile, the first three instructions, which are identical to those in the last example, use the cached image layers. However, because the changed RUN
instruction isn't cached, a new layer is created for the changed instruction and all subsequent instructions.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN mkdir test-1
RUN mkdir test-2
RUN mkdir test-5
RUN mkdir test-4
When you compare the image IDs of the new image to that in this section's first example, you'll notice that the first three layers from bottom to top are shared, but the fourth and fifth are unique.
docker history doc-sample-2
IMAGE CREATED CREATED BY SIZE COMMENT
c92cc95632fb 28 seconds ago cmd /S /C mkdir test-4 5.644 MB
2f05e6f5c523 37 seconds ago cmd /S /C mkdir test-5 5.01 MB
68fda53ce682 3 minutes ago cmd /S /C mkdir test-2 6.745 MB
5e5aa8ba1bc2 4 minutes ago cmd /S /C mkdir test-1 7.12 MB
6801d964fda5 5 months ago 0 B
Cosmetic optimization
Instruction case
Dockerfile instructions are not case-sensitive, but the convention is to use upper case. This improves readability by differentiating between the Instruction call and instruction operation. The following two examples compare an uncapitalized and capitalized Dockerfile.
The following is an uncapitalized Dockerfile:
# Sample Dockerfile
from mcr.microsoft.com/windows/servercore:ltsc2019
run dism /online /enable-feature /all /featurename:iis-webserver /NoRestart
run echo "Hello World - Dockerfile" > c:\inetpub\wwwroot\index.html
cmd [ "cmd" ]
The following is the same Dockerfile using upper-case:
# Sample Dockerfile
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN dism /online /enable-feature /all /featurename:iis-webserver /NoRestart
RUN echo "Hello World - Dockerfile" > c:\inetpub\wwwroot\index.html
CMD [ "cmd" ]
Line wrapping
Long and complex operations can be separated onto multiple lines by the backslash \
character. The following Dockerfile installs the Visual Studio Redistributable package, removes the installer files, and then creates a configuration file. These three operations are all specified on one line.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN powershell -Command c:\vcredist_x86.exe /quiet ; Remove-Item c:\vcredist_x86.exe -Force ; New-Item c:\config.ini
The command can be broken up with backslashes so that each operation from the one RUN
instruction is specified on its own line.
FROM mcr.microsoft.com/windows/servercore:ltsc2019
RUN powershell -Command \
$ErrorActionPreference = 'Stop'; \
Start-Process c:\vcredist_x86.exe -ArgumentList '/quiet' -Wait ; \
Remove-Item c:\vcredist_x86.exe -Force ; \
New-Item c:\config.ini