Building a single Docker image is straightforward, building multiple Docker images is slightly more difficult, but building multiple Docker images that depend on eachother is a daunting task. The latter is where a good build system can really help you.

The basic idea is to leverage a build system for building Docker images by creating a build target per Docker image and let the build system calculate (and execute) the dependency tree for you.

The build system I chose for this is NUKE. It's a relatively new kid on the block (2 years old) in the world of (.Net) build systems, where more commonly PSake, Cake, Fake, etc are used, but its ease-of-use is very promising and it has an active community. In short NUKE is a cross-platform build automation system with a C# DSL. NUKE basically creates a .Net Core console application that you run to perform your build. Having a C# build system seems to be a natural fit for Sitecore developers who typically write C# code.

Base and XP

Looking at the characteristics of a Sitecore Docker image build it consists of a number of images. As basis it has a number base images:

openjdk : Windows Server Core + OpenJDK

: Windows Server Core + OpenJDK sitecore : Windows Server Core + ASP.NET + commonly used tools (e.g. SIF, choco, vim)

: Windows Server Core + ASP.NET + commonly used tools (e.g. SIF, choco, vim) solr-build : Solr installation (on top of openjdk )

For each Docker image we define a NUKE target, e.g. for the openjdk image:

Target BaseOpenJdk => _ => _ .Executes(() => { DockerBuild(x => x .SetPath(".") .SetFile("base/openjdk/Dockerfile") .SetTag(BaseImageName("openjdk")) ); });

In above code snippet (full snippet here) we define a target name BaseOpenJdk and it perform a Docker build command. The similar Docker CLI command would be:

PS> docker build -file "base/openjdk/Dockerfile" -tag "openjdk" .

NUKE has the DockerBuild command built-in.

As you can see the solr-build image depends on the openjdk image. To specify this dependency we use the DependsOn clause for the solr-build target (full snippet here):

Target BaseSolrBuilder => _ => _ .DependsOn(BaseSitecore) .Executes(() => { ... DockerBuild(x => x ... .SetBuildArg(new string[] { $"BASE_IMAGE={baseImage}" }) ); });

NUKE will now build the openjdk target before the solr-builder target which is exactly what we need. As bonus we can now easily configure what the base image of the solr-builder is using a variable ( BASE_IMAGE ) in the Dockerfile. This gives us the flexibility to manage the versioniong (using Docker tags) in NUKE instead of having the Docker tags hard-coded in (multiple) Dockerfiles.

Above approach, using DependsOn , can be applied similarly to build the Sitecore XP Docker images (see here). Note that NUKE targets are grouped in separate C# files, each holding a partial C# class that inherits from NukeBuild .

XC (and Sitecore packages)

Things get more complicated when a running Sitecore system is required to create a Docker image, as is the case for Sitecore XC Docker images. A running Sitecore system is basically required because a Sitecore Package, e.g. Sitecore Commerce Connect, cannot be installed offline. To tackle this challenge during a NUKE build we;

start Sitecore using Docker Compose

install the the Sitecore package

stop using Docker Compose

commit the modified containers (e.g. Sitecore, MSSQL) as Docker image

For adding Commerce Connect to the XC Sitecore and MSSQL Docker image the code (full snippet here) looks as follows:

Target XcSitecoreMssql => _ => _ ... .DependsOn(XcCommerce, XcMssqlIntermediate, XcSitecoreIntermediate, XcSolr, XcXconnect) .Executes(() => { System.IO.Directory.SetCurrentDirectory("xc"); ... InstallSitecorePackage( @"C:\Scripts\InstallCommercePackages.ps1", XcImageName("sitecore"), XcImageName("mssql"), "-f docker-compose.yml" ); ... });

In above code we define that it depends on XC Commerce, Mssql, Sitecore, Solr and Xconnect targets (which produce each a Docker image) before the XcSitecoreMssql target can be executed. Note that we name the target/Docker images that are not final (and only used during build) as intermediate. Once the pre-conditions are met we perform InstallSitecorePackage , this performs the earlier mentioned steps:

private void InstallSitecorePackage(...) { DockerCompose($"{dockerComposeOptions} {DockerComposeSilenceOptions} down"); try { DockerCompose($"{dockerComposeOptions} {DockerComposeSilenceOptions} up -d"); // Install Commerce Connect package var sitecoreContainerName = GetContainerName("sitecore"); DockerExec(x => x .SetContainer(sitecoreContainerName) .SetCommand("powershell") .SetArgs(scriptFilename) ); DockerCompose($"{DockerComposeSilenceOptions} stop"); // Persist changes to DB installation directory DockerCompose($"{dockerComposeOptions} {DockerComposeSilenceOptions} up -d mssql"); ... // Commit changes DockerCommit(x => x .SetContainer(mssqlContainerName) .SetRepository(mssqlTargetImageName)); DockerCommit(x => x .SetContainer(sitecoreContainerName) .SetRepository(sitecoreTargetImageName)); } ... }

The full code for it can be found here

Full build execution plan

All the Sitecore Docker images together (base, XP, XC, and variants) result in a quite complex build order which isn't managable without a system, like NUKE, that calculates it for you. The current plan, without all Push targets (which are used to push the images to a Docker registry), looks as follows:

I hope this article, and the full setup available here, will help you to build your (Sitecore) Docker images and I look forward to receiving any feedback!