Sitecore 10 with Docker – Create a solution from the scratch – Setup Sitecore 10.3 XM setup with custom images

Change folder structure and configure basic Sitecore 10.3 XM setup with custom images
In our first part we spined up the XP0 environment provided by the getting started template.
Now we will configure docker, to use another folder structure and to spin up Sitecore XM.
Let’s first have a look on the .env file.
The .env file provides variables to the docker compose file when calling docker compose up -d
In it’s raw state the file looks as following:

Most of the values are self explaining. Just have a look here if you want to know more on how to set the necessary values.
After running the Init command in PowerShell it will look similar to

So the variables are prefilled to start docker compose.
We will extend and modify the .env and docker-compose.yml file and use a docker-compose.override.yml file to override the contents in docker-compose.yml
When looking into the custom-images folder of the docker-examples we see a different file and folder structure

Ignore for now the different docker-compose files as they are just for different topologies. . When comparing it to the Helix-Examples it looked pretty similar, why we are now following the same approach.

Change the folder structure
We start by creating a docker folder and moved the other folders into it

In the docker folder we rename mssql-data and solr-data to mssql & solr and move it into a separate data folder


We change the code in the clean.ps1 in the root folder to following, to match our new folder structure
# Clean data folders Get-ChildItem -Path (Join-Path $PSScriptRoot "docker\data\mssql") -Exclude ".gitkeep" -Recurse | Remove-Item -Force -Recurse -Verbose Get-ChildItem -Path (Join-Path $PSScriptRoot "docker\data\solr") -Exclude ".gitkeep" -Recurse | Remove-Item -Force -Recurse -Verbose
We change the code in Init.ps1 in the root folder to following, to match our new folder structure
Push-Location docker\traefik\certs
Let’s add some additional folders cm &cd to our data folder, which we will mount later for our log files
Next we add a new parameters to my .env file in the root folder, which we will use later as variable in our .yml files
LOCAL_DATA_PATH=.\docker\data

EDIT THE .ENV FILE
We have already added a new entry to our .env file.
We will do now some adjustments to our .env file to work in the further steps with the different variables.
#Isolation ISOLATION=default TRAEFIK_ISOLATION=hyperv #images TRAEFIK_IMAGE=traefik:v2.2.0-windowsservercore-1809 NETCORE_BUILD_IMAGE=mcr.microsoft.com/dotnet/sdk:6.0 SOLUTION_BUILD_IMAGE=mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2019 SOLUTION_BASE_IMAGE=mcr.microsoft.com/windows/nanoserver:1809 #hosts CM_HOST=cm.mydockerexperience.localhost CD_HOST=cd.mydockerexperience.localhost ID_HOST=id.mydockerexperience.localhost RENDERING_HOST=www.mydockerexperience.localhost #registry SITECORE_DOCKER_REGISTRY=scr.sitecore.com/sxp/ SITECORE_TOOLS_REGISTRY=scr.sitecore.com/tools/ SITECORE_MODULE_REGISTRY=scr.sitecore.com/sxp/modules/ #build BUILD_CONFIGURATION=debug #Sitecore config COMPOSE_PROJECT_NAME=sitecore-xm1 TOOLS_VERSION=10.2-1809 SITECORE_VERSION=10.3-ltsc2019 HOST_LICENSE_FOLDER=C:\Sitecore\License JSS_EDITING_SECRET=8E1FE24B-17B2-4AB3-B697-F191C408305A SOLR_CORE_PREFIX_NAME=sitecore #SC Modules SPE_VERSION=6.4-1809 SXA_VERSION=10.3-1809 HEADLESS_SERVICES_VERSION=21.0-1809 MANAGEMENT_SERVICES_VERSION=5.1.25-1809 #Passwords SITECORE_ADMIN_PASSWORD=Password12345 SQL_SA_PASSWORD=rM3Eow9jNrzlcuqs1X5 SQL_SERVER =mssql SQL_SA_LOGIN=sa SQL_DATABASE_PREFIX=Sitecore #Path configuration LOCAL_DEPLOY_PATH=.\docker\deploy LOCAL_DATA_PATH=.\docker\data EXTERNAL_IMAGE_TAG_SUFFIX=ltsc2019 #Sitecore id SITECORE_ID_CERTIFICATE<SITECORE_ID_CERTIFICATE> SITECORE_ID_CERTIFICATE_PASSWORD=<SITECORE_ID_CERTIFICATE_PASSWORD> # You should change the shared secret to a random string and not use the default value MEDIA_REQUEST_PROTECTION_SHARED_SECRET=>sNDw]9}hB~Z2SCwK1jtYUgY?5P2I-/)w]Jok3MTFez*xV*CWqBGG=j7&hn//<xi ##additional configuration TELERIK_ENCRYPTION_KEY=<TELERIK_ENCRYPTION_KEY> SITECORE_IDSECRET=<SITECORE_IDSECRET> SITECORE_LICENSE=<SITECORE_LICENSE_HASH>
Renew initialization
Let’s have a look what we have done in the previous step. (Let’s skip for now the things which need no explanation or change)
We changed our Host names and added a new one for our rendering host
#hosts CM_HOST=cm.mydockerexperience.localhost CD_HOST=cd.mydockerexperience.localhost ID_HOST=id.mydockerexperience.localhost RENDERING_HOST=www.mydockerexperience.localhost
Here we need to do some adjustments to our current files.
We open .\docker\traefik\config\certs_config.yaml

We change the content from
tls: certificates: - certFile: C:\etc\traefik\certs\xp0cm.localhost.crt keyFile: C:\etc\traefik\certs\xp0cm.localhost.key - certFile: C:\etc\traefik\certs\xp0id.localhost.crt keyFile: C:\etc\traefik\certs\xp0id.localhost.key
to
tls: certificates: - certFile: C:\etc\traefik\certs\_wildcard.mydockerexperience.localhost.pem keyFile: C:\etc\traefik\certs\_wildcard.mydockerexperience.localhost-key.pem
In the root folder we create a docker-compose.override.yml file and added following
What we did is telling our traefik container to consume a wildcard certificate from C:\etc\traefik\certs_wildcard.mydockerexperience.localhost.pem.
BUT wait!!! We have not yet created the certificate.
Let’s go back to our Init.ps1 in our root folder and search for following entry
Write-Host "Generating Traefik TLS certificates..." -ForegroundColor Green & $mkcert -install & $mkcert -cert-file xp0cm.localhost.crt -key-file xp0cm.localhost.key "xp0cm.localhost" & $mkcert -cert-file xp0cd.localhost.crt -key-file xp0cd.localhost.key "xp0cd.localhost" & $mkcert -cert-file xp0id.localhost.crt -key-file xp0id.localhost.key "xp0id.localhost"
Let’s replace this by
Write-Host "Generating Traefik TLS certificates..." -ForegroundColor Green & $mkcert -install & $mkcert "*.mydockerexperience.localhost"
Add the bottom of the Init.ps1 file we find following
Write-Host "Adding Windows hosts file entries..." -ForegroundColor Green Add-HostsEntry "xp0cm.localhost" Add-HostsEntry "xp0id.localhost" Add-HostsEntry "xp0cd.localhost"
We replace it with our new hosts
Write-Host "Adding Windows hosts file entries..." -ForegroundColor Green Add-HostsEntry "cm.mydockerexperience.localhost" Add-HostsEntry "id.mydockerexperience.localhost" Add-HostsEntry "cd.mydockerexperience.localhost" Add-HostsEntry "www.mydockerexperience.localhost"
After that we can call the Init.ps1 in PowerShell again to add the new host entries and to create the new certificate.
.\init.ps1 -LicenseXmlPath "<your licensepath here>"
Configure docker-compose & docker-compose.override.yml
In the root folder we open the docker-compose.yml file and clean the whole content, beside the following:
version: "2.4" services:
We will now configure step by step the different instances for our docker file.
Traefik
traefik: isolation: ${TRAEFIK_ISOLATION} image: ${TRAEFIK_IMAGE} command: - "--ping" - "--api.insecure=true" - "--providers.docker.endpoint=npipe:////./pipe/docker_engine" - "--providers.docker.exposedByDefault=false" - "--providers.file.directory=C:/etc/traefik/config/dynamic" - "--entryPoints.websecure.address=:443" ports: - "443:443" - "8079:8080" healthcheck: test: ["CMD", "traefik", "healthcheck", "--ping"] volumes: - source: \\.\pipe\docker_engine target: \\.\pipe\docker_engine type: npipe - ./traefik:C:/etc/traefik depends_on: id: condition: service_healthy cd: condition: service_healthy cm: condition: service_healthy
The depends_on configuration is for later usage where we control how docker will start and when the added service is ready(In this case id,cm,cd).
We are using 2 variables
isolation: ${TRAEFIK_ISOLATION}
image: ${TRAEFIK_IMAGE}
which we can find in our .env file in the root folder

Notice:
volumes:
- source: \\.\pipe\docker_engine
target: \\.\pipe\docker_engine
type: npipe
- ./traefik:C:/etc/traefik
This mounts the traefic folder from our root ./traefik to C:/etc/trafic in our docker image. As we have changed the folder structure we need to override this value, to point to our new structure. Therefore we create a docker-compose.override.yml in our root folder.
The docker-compose.override.yml overrides configuration done in the docker-compose.yml.
In this file we add following
version: "2.4" services: traefik: volumes: - ./docker/traefik:C:/etc/traefik depends_on: - rendering
We have now overridden the docker-compose.yml entry of trafic to mount our new folder structure.
volumes:
- ./docker/traefik:C:/etc/traefik
Remember we configured this path for the certificate used by the traefik container.
We will now configure our images to spin up xm by adding the necessary configuration to docker-compose & docker-compose.override
Redis
docker-compose.yml
redis: isolation: ${ISOLATION} image: ${SITECORE_DOCKER_REGISTRY}redis:3.2.100-${EXTERNAL_IMAGE_TAG_SUFFIX} mssql: isolation: ${ISOLATION} image: ${SITECORE_DOCKER_REGISTRY}nonproduction/mssql-developer:2017-${EXTERNAL_IMAGE_TAG_SUFFIX} environment: SA_PASSWORD: ${SQL_SA_PASSWORD} SITECORE_ADMIN_PASSWORD: ${SITECORE_ADMIN_PASSWORD} ACCEPT_EULA: "Y" ports: - "14330:1433" volumes: - type: bind source: .\mssql-data target: c:\data
We are using the previously created parameters in this configuration. We are using nonproduction/mssql-developer:2017-${EXTERNAL_IMAGE_TAG_SUFFIX} just because we are creating a XM 10.3 environment and at this point in time there are no production images available in Sitecore Image repository.
docker-compose.override.yml
redis: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-redis:${VERSION:-latest} build: context: ./docker/build/redis args: BASE_IMAGE: ${SITECORE_DOCKER_REGISTRY}redis:3.2.100-${EXTERNAL_IMAGE_TAG_SUFFIX}
Here is something new!
image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-redis:${VERSION:-latest}
With this entry we are defining a name for a new custom image for docker
The name will be something like sitecore-xm1-redis:latest
build: context: ./docker/build/redis
Here we are pointing to a subfolder of our docker folder.
This folder does not exist yet, so let’s create it


In this folder we add a DockerFile, which will be using the args parameters and as a basis for our custom images.
args: BASE_IMAGE: ${SITECORE_DOCKER_REGISTRY}redis:3.2.100-${EXTERNAL_IMAGE_TAG_SUFFIX}

The DockerFile in this case is very simple. It uses the BASE_IMAGE to build a custom image
# escape=` ARG BASE_IMAGE FROM ${BASE_IMAGE}
For now we will create the folders cd,cm,dotnetsdk,id,mssql-init,redis,rendering and solr-init and place a DockerFile with the same content in it.

These will be the custom images which we will create in the next steps.
MSSQL
docker-compose.yml
mssql: isolation: ${ISOLATION} image: ${SITECORE_DOCKER_REGISTRY}nonproduction/mssql-developer:2017-${EXTERNAL_IMAGE_TAG_SUFFIX} environment: SA_PASSWORD: ${SQL_SA_PASSWORD} SITECORE_ADMIN_PASSWORD: ${SITECORE_ADMIN_PASSWORD} ACCEPT_EULA: "Y" ports: - "14330:1433" volumes: - type: bind source: .\mssql-data target: c:\data
docker-compose.override.yml
mssql: mem_limit: 2GB volumes: - ${LOCAL_DATA_PATH}\mssql:c:\data
Solr
docker-compose.yml
solr: isolation: ${ISOLATION} image: ${SITECORE_DOCKER_REGISTRY}nonproduction/solr:8.11.2-${EXTERNAL_IMAGE_TAG_SUFFIX} ports: - "8984:8983" volumes: - type: bind source: .\solr-data target: c:\data environment: SOLR_MODE: solrcloud healthcheck: test: ["CMD", "powershell", "-command", "try { $$statusCode = (iwr http://solr:8983/solr/admin/cores?action=STATUS -UseBasicParsing).StatusCode; if ($$statusCode -eq 200) { exit 0 } else { exit 1} } catch { exit 1 }"]
docker-compose.override.yml
# Mount our Solr data folder. solr: volumes: - ${LOCAL_DATA_PATH}\solr:c:\data
solr-init
docker-compose.yml
solr-init: isolation: ${ISOLATION} image: ${SITECORE_DOCKER_REGISTRY}sitecore-xm1-solr-init:${SITECORE_VERSION} environment: SITECORE_SOLR_CONNECTION_STRING: http://solr:8983/solr SOLR_CORE_PREFIX_NAME: ${SOLR_CORE_PREFIX_NAME} depends_on: solr: condition: service_healthy
docker-compose.override.yml
# Mount our Solr data folder and use our retagged Solr image. # Some modules (like SXA) also require additions to the Solr image. solr-init: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-xm1-solr-init:${VERSION:-latest} build: context: ./docker/build/solr-init args: BASE_IMAGE: ${SITECORE_DOCKER_REGISTRY}sitecore-xm1-solr-init:${SITECORE_VERSION}
Identityserver
docker-compose.yml
id: isolation: ${ISOLATION} image: ${SITECORE_DOCKER_REGISTRY}sitecore-id7:${SITECORE_VERSION} environment: Sitecore_Sitecore__IdentityServer__SitecoreMemberShipOptions__ConnectionString: Data Source=mssql;Initial Catalog=Sitecore.Core;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_Sitecore__IdentityServer__AccountOptions__PasswordRecoveryUrl: https://${CM_HOST}/sitecore/login?rc=1 Sitecore_Sitecore__IdentityServer__Clients__PasswordClient__ClientSecrets__ClientSecret1: ${SITECORE_IDSECRET} Sitecore_Sitecore__IdentityServer__Clients__DefaultClient__AllowedCorsOrigins__AllowedCorsOriginsGroup1: https://${CM_HOST} Sitecore_Sitecore__IdentityServer__CertificateRawData: ${SITECORE_ID_CERTIFICATE} Sitecore_Sitecore__IdentityServer__PublicOrigin: https://${ID_HOST} Sitecore_Sitecore__IdentityServer__CertificateRawDataPassword: ${SITECORE_ID_CERTIFICATE_PASSWORD} Sitecore_License: ${SITECORE_LICENSE} healthcheck: test: ["CMD", "pwsh", "-command", "C:/Healthchecks/Healthcheck.ps1"] timeout: 300s depends_on: mssql: condition: service_healthy labels: - "traefik.enable=true" - "traefik.http.routers.id-secure.entrypoints=websecure" - "traefik.http.routers.id-secure.rule=Host(`${ID_HOST}`)" - "traefik.http.routers.id-secure.tls=true"
docker-compose.override.yml
# Use our retagged Identity Server image. # Configure for a mounted license file instead of using SITECORE_LICENSE. id: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-id7:${VERSION:-latest} build: context: ./docker/build/id args: BASE_IMAGE: ${SITECORE_DOCKER_REGISTRY}sitecore-id7:${SITECORE_VERSION} volumes: - ${HOST_LICENSE_FOLDER}:c:\license environment: SITECORE_LICENSE_LOCATION: c:\license\license.xml
Content delivery
docker-compose.yml
cd: isolation: ${ISOLATION} image: ${SITECORE_DOCKER_REGISTRY}sitecore-xm1-cd:${SITECORE_VERSION} depends_on: mssql: condition: service_healthy solr-init: condition: service_started redis: condition: service_started environment: Sitecore_AppSettings_instanceNameMode:define: default Sitecore_ConnectionStrings_Security: Data Source=mssql;Initial Catalog=Sitecore.Core;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_ConnectionStrings_Web: Data Source=mssql;Initial Catalog=Sitecore.Web;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_ConnectionStrings_ExperienceForms: Data Source=mssql;Initial Catalog=Sitecore.ExperienceForms;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_ConnectionStrings_Solr.Search: http://solr:8983/solr;solrCloud=true Sitecore_ConnectionStrings_Redis.Sessions: redis:6379,ssl=False,abortConnect=False Sitecore_License: ${SITECORE_LICENSE} SOLR_CORE_PREFIX_NAME: ${SOLR_CORE_PREFIX_NAME} MEDIA_REQUEST_PROTECTION_SHARED_SECRET: ${MEDIA_REQUEST_PROTECTION_SHARED_SECRET} healthcheck: test: ["CMD", "powershell", "-command", "C:/Healthchecks/Healthcheck.ps1"] timeout: 300s labels: - "traefik.enable=true" - "traefik.http.routers.cd-secure.entrypoints=websecure" - "traefik.http.routers.cd-secure.rule=Host(`${CD_HOST}`)" - "traefik.http.routers.cd-secure.tls=true"
docker-compose.override.yml
cd: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-xm1-cd:${VERSION:-latest} build: context: ./docker/build/cd args: BASE_IMAGE: ${SITECORE_DOCKER_REGISTRY}sitecore-xm1-cd:${SITECORE_VERSION} depends_on: - solution volumes: - ${LOCAL_DATA_PATH}\cd:C:\inetpub\wwwroot\App_Data\logs
Notice: That we mounted the sitecore logs to our data folder
Content management
docker-compose.yml
cm: isolation: ${ISOLATION} image: ${SITECORE_DOCKER_REGISTRY}sitecore-xm1-cm:${SITECORE_VERSION} depends_on: mssql: condition: service_healthy solr-init: condition: service_started id: condition: service_started environment: Sitecore_AppSettings_instanceNameMode:define: default Sitecore_ConnectionStrings_Core: Data Source=mssql;Initial Catalog=Sitecore.Core;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_ConnectionStrings_Security: Data Source=mssql;Initial Catalog=Sitecore.Core;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_ConnectionStrings_Master: Data Source=mssql;Initial Catalog=Sitecore.Master;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_ConnectionStrings_Web: Data Source=mssql;Initial Catalog=Sitecore.Web;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_ConnectionStrings_ExperienceForms: Data Source=mssql;Initial Catalog=Sitecore.ExperienceForms;User ID=sa;Password=${SQL_SA_PASSWORD} Sitecore_ConnectionStrings_Solr.Search: http://solr:8983/solr;solrCloud=true Sitecore_ConnectionStrings_Sitecoreidentity.secret: ${SITECORE_IDSECRET} Sitecore_AppSettings_Telerik.AsyncUpload.ConfigurationEncryptionKey: ${TELERIK_ENCRYPTION_KEY} Sitecore_AppSettings_Telerik.Upload.ConfigurationHashKey: ${TELERIK_ENCRYPTION_KEY} Sitecore_AppSettings_Telerik.Web.UI.DialogParametersEncryptionKey: ${TELERIK_ENCRYPTION_KEY} Sitecore_License: ${SITECORE_LICENSE} Sitecore_Identity_Server_Authority: https://${ID_HOST} Sitecore_Identity_Server_InternalAuthority: http://id Sitecore_Identity_Server_CallbackAuthority: https://${CM_HOST} Sitecore_Identity_Server_Require_Https: "false" SOLR_CORE_PREFIX_NAME: ${SOLR_CORE_PREFIX_NAME} MEDIA_REQUEST_PROTECTION_SHARED_SECRET: ${MEDIA_REQUEST_PROTECTION_SHARED_SECRET} healthcheck: test: ["CMD", "powershell", "-command", "C:/Healthchecks/Healthcheck.ps1"] timeout: 300s labels: - "traefik.enable=true" - "traefik.http.middlewares.force-STS-Header.headers.forceSTSHeader=true" - "traefik.http.middlewares.force-STS-Header.headers.stsSeconds=31536000" - "traefik.http.routers.cm-secure.entrypoints=websecure" - "traefik.http.routers.cm-secure.rule=Host(`${CM_HOST}`)" - "traefik.http.routers.cm-secure.tls=true" - "traefik.http.routers.cm-secure.middlewares=force-STS-Header"
docker-compose.override.yml
cm: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-xm1-cm:${VERSION:-latest} build: context: ./docker/build/cm args: BASE_IMAGE: ${SITECORE_DOCKER_REGISTRY}sitecore-xm1-cm:${SITECORE_VERSION} depends_on: - solution volumes: - ${LOCAL_DATA_PATH}\cm:C:\inetpub\wwwroot\App_Data\logs
Notice: That we mounted the Sitecore logs to our data folder
If we call
docker-compose build docker-compose up -d
from PowerShell our custom images will build and spin up as Sitecore XM, but it will be still a Vanilla Instance without any modules installed.
In the next part we are going to add Sitecore modules and Sitecore Serialization to our instance