Sitecore 10 with Docker – Create a solution from the scratch – Add ASP.NET Rendering Host and configure SXA-JSS

Add renderinghost
We want to develop headless and make use of the rendering host.
Add docker build
“With a typical Sitecore implementation, a single Visual Studio solution usually creates build artifacts for multiple roles, for example, Website and XConnect assemblies, and some artifacts must be deployed to multiple roles (for example CM/CD). For containers, this means that the same build output must be layered on top of multiple base Sitecore runtime images. You could instead duplicate the build instructions for each Sitecore image, but this would be very inefficient.
What you need is a Dockerfile that is focused solely on building your solution and storing the output as structured build artifacts on the resulting image.” (source)
So we will add a new image which builds our solution and creates artifacts.
We have already configured the necessary parameters in our .env file
#This image contains the .NET SDK which is comprised of three parts: #.NET CLI #.NET runtime #ASP.NET Core NETCORE_BUILD_IMAGE=mcr.microsoft.com/dotnet/sdk:6.0 #.NET Framework Runtime #Visual Studio Build Tools #Visual Studio Test Agent #NuGet CLI #.NET Framework Targeting Packs #ASP.NET Web Targets SOLUTION_BUILD_IMAGE=mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2019 #This is a base image for Windows Server containers. This image carries the Nano Server base OS image. SOLUTION_BASE_IMAGE=mcr.microsoft.com/windows/nanoserver:1809 BUILD_CONFIGURATION=debug
For the Docker.build image we add in our docker-compose.override.yml we add following
solution: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest} build: context: . args: BASE_IMAGE: ${SOLUTION_BASE_IMAGE} BUILD_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-dotnetsdk:${VERSION:-latest} BUILD_CONFIGURATION: ${BUILD_CONFIGURATION} depends_on: - dotnetsdk scale: 0
Now we will create a custom SDK image based on servercore that serves two purposes:
- Allows us to build a mixed solution (framework and netcore)
- Allows us to run `dotnet watch` for rendering host development
In our docker-compose.override.yml we add following
dotnetsdk: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-dotnetsdk:${VERSION:-latest} build: context: ./docker/build/dotnetsdk args: BUILD_IMAGE: ${SOLUTION_BUILD_IMAGE} NETCORE_BUILD_IMAGE: ${NETCORE_BUILD_IMAGE} scale: 0
We change the content of the dotnetsdk DockerFile in our build folder to following
# escape=` # This is a custom SDK image based on servercore that serves two purposes: # * Allows us to build a mixed solution (framework and netcore) # * Allows us to run `dotnet watch` for rendering host development # (see https://github.com/dotnet/dotnet-docker/issues/1984) ARG BUILD_IMAGE ARG NETCORE_BUILD_IMAGE FROM ${NETCORE_BUILD_IMAGE} as netcore-sdk FROM ${BUILD_IMAGE} SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] # Ensure updated nuget. Depending on your Windows version, dotnet/framework/sdk:4.8 tag may provide an outdated client. # See https://github.com/microsoft/dotnet-framework-docker/blob/1c3dd6638c6b827b81ffb13386b924f6dcdee533/4.8/sdk/windowsservercore-ltsc2019/Dockerfile#L7 ENV NUGET_VERSION 5.8.0 RUN Invoke-WebRequest "https://dist.nuget.org/win-x86-commandline/v$env:NUGET_VERSION/nuget.exe" -UseBasicParsing -OutFile "$env:ProgramFiles\NuGet\nuget.exe" ## Install netcore onto SDK image ## https://github.com/dotnet/dotnet-docker/blob/5e9b849a900c69edfe78f6e0f3519009de4ab471/3.1/sdk/nanoserver-1909/amd64/Dockerfile # Retrieve .NET Core SDK COPY --from=netcore-sdk ["/Program Files/dotnet/", "/Program Files/dotnet/"] ENV ` # Enable detection of running in a container DOTNET_RUNNING_IN_CONTAINER=true ` # Enable correct mode for dotnet watch (only mode supported in a container) DOTNET_USE_POLLING_FILE_WATCHER=true ` # Skip extraction of XML docs - generally not useful within an image/container - helps performance NUGET_XMLDOC_MODE=skip RUN $path = ${Env:PATH} + ';C:\Program Files\dotnet\;'; ` setx /M PATH $path # Trigger first run experience by running arbitrary cmd RUN dotnet help | out-null
Let’s create a blank DockerFile in the root folder and add following content
Now we add a new Dockerfile for our custom image to our root folder, which builds the solution and moves the artifacts.
# escape=` ARG BASE_IMAGE ARG BUILD_IMAGE FROM ${BUILD_IMAGE} AS prep # Gather only artifacts necessary for NuGet restore, retaining directory structure COPY *.sln nuget.config Directory.Build.targets Packages.props \nuget\ COPY src\ \temp\ RUN Invoke-Expression 'robocopy C:\temp C:\nuget\src /s /ndl /njh /njs *.csproj *.scproj packages.config' FROM ${BUILD_IMAGE} AS builder ARG BUILD_CONFIGURATION SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] # Create an empty working directory WORKDIR C:\build # Copy prepped NuGet artifacts, and restore as distinct layer to take better advantage of caching COPY --from=prep .\nuget .\ RUN nuget restore -Verbosity quiet # Copy remaining source code COPY src\ .\src\ # Build the Sitecore main platform artifacts RUN msbuild .\src\Project\Platform\website\MyDockerExperience.Platform.csproj /p:Configuration=$env:BUILD_CONFIGURATION /p:DeployOnBuild=True /p:DeployDefaultTarget=WebPublish /p:WebPublishMethod=FileSystem /p:PublishUrl=C:\out\sitecore FROM ${BASE_IMAGE} WORKDIR C:\artifacts # Copy final build artifacts COPY --from=builder C:\out\sitecore .\sitecore\
In the docker-compose.override.yml we add additional args parameter for CD & CM
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} SPE_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-spe-assets:${SPE_VERSION} SXA_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-sxa-xm1-assets:${SXA_VERSION} HEADLESS_SERVICES_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-headless-services-xm1-assets:${HEADLESS_SERVICES_VERSION} MANAGEMENT_SERVICES_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-management-services-xm1-assets:${MANAGEMENT_SERVICES_VERSION} TOOLING_IMAGE: ${SITECORE_TOOLS_REGISTRY}sitecore-docker-tools-assets:${TOOLS_VERSION} SOLUTION_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest} volumes: - ${LOCAL_DEPLOY_PATH}\platform:C:\deploy - ${LOCAL_DATA_PATH}\cm:C:\inetpub\wwwroot\App_Data\logs entrypoint: powershell -Command "& C:\\tools\\entrypoints\\iis\\Development.ps1" 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} SXA_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-sxa-xm1-assets:${SXA_VERSION} HEADLESS_SERVICES_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-headless-services-xm1-assets:${HEADLESS_SERVICES_VERSION} TOOLING_IMAGE: ${SITECORE_TOOLS_REGISTRY}sitecore-docker-tools-assets:${TOOLS_VERSION} SOLUTION_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest} depends_on: - solution volumes: - ${LOCAL_DEPLOY_PATH}\platform:C:\deploy - ${LOCAL_DATA_PATH}\cd:C:\inetpub\wwwroot\App_Data\logs entrypoint: powershell -Command "& C:\\tools\\entrypoints\\iis\\Development.ps1"
To use this additional parameter in our build we need to adjust our Dockerfiles for CM & CD
CM:
ARG SOLUTION_IMAGE FROM ${SOLUTION_IMAGE} as solution # Copy solution website files COPY --from=solution \artifacts\sitecore\ .\
# escape=` ARG BASE_IMAGE ARG SXA_IMAGE ARG SPE_IMAGE ARG MANAGEMENT_SERVICES_IMAGE ARG HEADLESS_SERVICES_IMAGE ARG TOOLING_IMAGE ARG SOLUTION_IMAGE FROM ${SOLUTION_IMAGE} as solution FROM ${SPE_IMAGE} as spe FROM ${SXA_IMAGE} as sxa FROM ${HEADLESS_SERVICES_IMAGE} AS headless_services FROM ${MANAGEMENT_SERVICES_IMAGE} AS management_services FROM ${TOOLING_IMAGE} as tooling FROM ${BASE_IMAGE} # Copy development tools and entrypoint COPY --from=tooling \tools\ \tools\ WORKDIR C:\inetpub\wwwroot SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] COPY --from=spe \module\cm\content .\ COPY --from=sxa \module\cm\content .\ COPY --from=sxa \module\tools \module\tools RUN C:\module\tools\Initialize-Content.ps1 -TargetPath .\; ` Remove-Item -Path C:\module -Recurse -Force; # Copy the Sitecore Management Services Module COPY --from=management_services C:\module\cm\content C:\inetpub\wwwroot # Copy and init the JSS / Headless Services Module COPY --from=headless_services C:\module\cm\content C:\inetpub\wwwroot COPY --from=headless_services C:\module\tools C:\module\tools RUN C:\module\tools\Initialize-Content.ps1 -TargetPath C:\inetpub\wwwroot; ` Remove-Item -Path C:\module -Recurse -Force; # Copy solution website files COPY --from=solution \artifacts\sitecore\ .\
CD:
# escape=` ARG BASE_IMAGE ARG SXA_IMAGE ARG TOOLING_IMAGE ARG HEADLESS_SERVICES_IMAGE ARG SOLUTION_IMAGE FROM ${SOLUTION_IMAGE} as solution FROM ${SXA_IMAGE} as sxa FROM ${HEADLESS_SERVICES_IMAGE} AS headless_services FROM ${TOOLING_IMAGE} as tooling FROM ${BASE_IMAGE} SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] # Copy development tools and entrypoint COPY --from=tooling \tools\ \tools\ WORKDIR C:\inetpub\wwwroot # Add SXA module COPY --from=sxa \module\cd\content .\ COPY --from=sxa \module\tools \module\tools RUN C:\module\tools\Initialize-Content.ps1 -TargetPath .\; ` Remove-Item -Path C:\module -Recurse -Force; # Copy and init the JSS / Headless Services Module COPY --from=headless_services C:\module\cd\content C:\inetpub\wwwroot COPY --from=headless_services C:\module\tools C:\module\tools RUN C:\module\tools\Initialize-Content.ps1 -TargetPath C:\inetpub\wwwroot; ` Remove-Item -Path C:\module -Recurse -Force; # Copy solution website files COPY --from=solution \artifacts\sitecore\ .\
We add a package.props file to our root
<?xml version="1.0" encoding="utf-8"?> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <PlatformVersion>10.3.0</PlatformVersion> <SitecoreAspNetVersion>20.0.0</SitecoreAspNetVersion> </PropertyGroup> <ItemGroup> <PackageReference Update="Sitecore.Nexus" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.Kernel" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.Mvc" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.ContentSearch" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.ContentSearch.Linq" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.ContentSearch.ContentExtraction" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.Assemblies.Platform" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.LayoutService.Client" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Sitecore.AspNet.RenderingEngine" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Sitecore.AspNet.ExperienceEditor" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Sitecore.AspNet.Tracking" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Sitecore.AspNet.Tracking.VisitorIdentification" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(Net6x)" /> <PackageReference Update="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(Net6x)" /> <PackageReference Update="Microsoft.Extensions.Http" Version="$(Net6x)" /> <PackageReference Update="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="$(Net6x)" /> </ItemGroup> </Project>
To get more information about package.props visit https://timdeschryver.dev/blog/directorypackagesprops-a-solution-to-unify-your-nuget-package-versions
We configure nuget, so that we have a nuget.config file in our root which should look similar to
<?xml version="1.0" encoding="utf-8"?> <configuration> <!-- Used to specify the default Sources for list, install and update. --> <packageSources> <clear /> <add key="Nuget" value="https://api.nuget.org/v3/index.json" /> <add key="Sitecore" value="https://sitecore.myget.org/F/sc-packages/api/v3/index.json" /> </packageSources> <activePackageSource> <!-- this tells that all of them are active --> <add key="All" value="(Aggregate source)" /> </activePackageSource> </configuration>
We add a directory.build.targets file to our root (more information https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-your-build?view=vs-2022)
directory.build.targets
<!-- This targets file will enable Central Package Versions for any Visual Studio projects in the solution. https://github.com/microsoft/MSBuildSdks/tree/master/src/CentralPackageVersions --> <Project> <Sdk Name="Microsoft.Build.CentralPackageVersions" Version="2.0.79" /> </Project>
Add rendering host project to visual studio
We create a new ASP.NET Core Web App “MyDockerExperience.Rendering” in our Project folder.




Add rendering host IMAGE TO docker
Next we add our custom rendering host container configuration to docker-compose.override.yml
rendering: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-rendering:${VERSION:-latest} build: context: ./docker/build/rendering target: ${BUILD_CONFIGURATION} args: DEBUG_BASE_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-dotnetsdk:${VERSION:-latest} RELEASE_BASE_IMAGE: mcr.microsoft.com/dotnet/sdk:6.0 SOLUTION_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest} ports: - "8090:80" volumes: - .\:C:\solution environment: ASPNETCORE_ENVIRONMENT: "Development" ASPNETCORE_URLS: "http://*:80" Sitecore__InstanceUri: "http://cd" Sitecore__EnableExperienceEditor: "true" Sitecore__RenderingHostUri: "https://${RENDERING_HOST}" JSS_EDITING_SECRET: ${JSS_EDITING_SECRET} depends_on: - solution - cd labels: - "traefik.enable=true" - "traefik.http.routers.rendering-secure.entrypoints=websecure" - "traefik.http.routers.rendering-secure.rule=Host(`${RENDERING_HOST}`)" - "traefik.http.routers.rendering-secure.tls=true"
We add the JSS_EDITING_SECRET & RENDERING_HOST_PUBLIC_URI to our CM configuration .
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} SPE_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-spe-assets:${SPE_VERSION} SXA_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-sxa-xm1-assets:${SXA_VERSION} HEADLESS_SERVICES_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-headless-services-xm1-assets:${HEADLESS_SERVICES_VERSION} MANAGEMENT_SERVICES_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-management-services-xm1-assets:${MANAGEMENT_SERVICES_VERSION} TOOLING_IMAGE: ${SITECORE_TOOLS_REGISTRY}sitecore-docker-tools-assets:${TOOLS_VERSION} SOLUTION_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest} depends_on: - solution volumes: - ${LOCAL_DEPLOY_PATH}\platform:C:\deploy - ${LOCAL_DATA_PATH}\cm:C:\inetpub\wwwroot\App_Data\logs environment: RENDERING_HOST_PUBLIC_URI: "https://${RENDERING_HOST}" SITECORE_JSS_EDITING_SECRET: ${JSS_EDITING_SECRET} entrypoint: powershell -Command "& C:\\tools\\entrypoints\\iis\\Development.ps1"
Again we add a new custom build image for our rendering container to our file system
# escape=` # This is an example Dockerfile for an ASP.NET Core Rendering Host. # We use build stages to enable 'dotnet watch' during development, so # that changes to your rendering code can be quickly tested, including in # the Experience Editor. Be sure to watch the container logs in case of build errors. ARG DEBUG_BASE_IMAGE ARG RELEASE_BASE_IMAGE ARG SOLUTION_IMAGE FROM ${DEBUG_BASE_IMAGE} as debug WORKDIR /solution/src ENV DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER=true EXPOSE 80 ENTRYPOINT ["dotnet", "watch", "-v", "--project", ".\\Project\\rendering", "run", "--no-launch-profile"] FROM ${SOLUTION_IMAGE} as solution FROM ${RELEASE_BASE_IMAGE} as release WORKDIR /app COPY --from=solution /artifacts/rendering/ ./ EXPOSE 80 ENTRYPOINT ["dotnet", "MyDockerExperience.Rendering.dll"]
We need to create the artifact in our solution build file in our root dockerfile as well
# escape=` ARG BASE_IMAGE ARG BUILD_IMAGE FROM ${BUILD_IMAGE} AS prep # Gather only artifacts necessary for NuGet restore, retaining directory structure COPY *.sln nuget.config Directory.Build.targets Packages.props \nuget\ COPY src\ \temp\ RUN Invoke-Expression 'robocopy C:\temp C:\nuget\src /s /ndl /njh /njs *.csproj *.scproj packages.config' FROM ${BUILD_IMAGE} AS builder ARG BUILD_CONFIGURATION SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] # Create an empty working directory WORKDIR C:\build # Copy prepped NuGet artifacts, and restore as distinct layer to take better advantage of caching COPY --from=prep .\nuget .\ RUN nuget restore -Verbosity quiet # Copy remaining source code COPY src\ .\src\ # Build the Sitecore main platform artifacts RUN msbuild .\src\Project\Platform\website\MyDockerExperience.Platform.csproj /p:Configuration=$env:BUILD_CONFIGURATION /p:DeployOnBuild=True /p:DeployDefaultTarget=WebPublish /p:WebPublishMethod=FileSystem /p:PublishUrl=C:\out\sitecore # Build the rendering host WORKDIR C:\build\src\Project\rendering\ RUN dotnet restore RUN dotnet publish -c $env:BUILD_CONFIGURATION -o C:\out\rendering --no-restore FROM ${BASE_IMAGE} WORKDIR C:\artifacts # Copy final build artifacts COPY --from=builder C:\out\sitecore .\sitecore\ COPY --from=builder C:\out\rendering .\rendering\
CONFIGURE RENDERING HOST & START WORKING
Now as our image is ready we can start configuring our environment to work with our rendering host.
- Preparing the urls, certificates, & init.ps1
- Configuring the CM to communicate with the rendering host
- Configure the rendering host project
PrePARING THE URLS
We already did the most of the changes previously.
Let’s have a look on what we did.
In our .env file we changed the CM, CD & Identitiserver host entries and added a new variable for our rendering host
CM_HOST=cm.mydockerexperience.localhost CD_HOST=cd.mydockerexperience.localhost ID_HOST=id.mydockerexperience.localhost RENDERING_HOST=www.mydockerexperience.localhost
In the init.ps1 file we changed our init.ps1 create a wildcard certificate
Write-Host "Generating Traefik TLS certificates..." -ForegroundColor Green $mkcert "*.mydockerexperience.localhost"
and added the new host entries (you can remove the old ones from your hosts file)
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"
Finally we changed \docker\traefik\config\dynamic\certs_config.yaml
tls: certificates: - certFile: C:\etc\traefik\certs\_wildcard.mydockerexperience.localhost.pem keyFile: C:\etc\traefik\certs\_wildcard.mydockerexperience.localhost-key.pem
Run docker-compose build
Run docker-compose up -d
We can verify our changes

Connect the dots
Now we will add a connection between our environments
In the docker-compose.override.yml we add a new environment variable
cm:
image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-xp0-cm:${VERSION:-latest}
build:
context: ./docker/build/cm
args:
BASE_IMAGE: ${SITECORE_DOCKER_REGISTRY}sitecore-xp0-cm:${SITECORE_VERSION}
SPE_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-spe-assets:${SPE_VERSION}
SXA_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-sxa-xp1-assets:${SXA_VERSION}
TOOLING_IMAGE: ${SITECORE_TOOLS_REGISTRY}sitecore-docker-tools-assets:${TOOLS_VERSION}
HEADLESS_SERVICES_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-headless-services-xm1-assets:${HEADLESS_SERVICES_VERSION}
MANAGEMENT_SERVICES_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-management-services-xm1-assets:${MANAGEMENT_SERVICES_VERSION}
SOLUTION_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest}
volumes:
- ${LOCAL_DEPLOY_PATH}\platform:C:\deploy
entrypoint: powershell -Command "& C:\\tools\\entrypoints\\iis\\Development.ps1"
environment:
RENDERING_HOST_PUBLIC_URI: "https://${RENDERING_HOST}"
depends_on:
- solution
Let’s go to sitecore and create the necessary items for our project.
Create an API KEy
For easyness we allow everything in our API Key. In Production environments we normally want to configure our API keys in detail. (More information)

In this example we will use sxa-jss, so we do not need to create configuration files in our solution as in most common tutorials
We create a new Headless Tenant “Docker Experience” and Headless Site “MyDockerExperience”




To make SXA-JSS working with ASP.NET Renderinghost we do the following

We extend our package props with our Core version and configure some additional packages
<?xml version="1.0" encoding="utf-8"?> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <PlatformVersion>10.3.0</PlatformVersion> <SitecoreAspNetVersion>20.0.0</SitecoreAspNetVersion> <Net6x>6.0.0</Net6x> </PropertyGroup> <ItemGroup> <PackageReference Update="Sitecore.Nexus" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.Kernel" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.Mvc" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.ContentSearch" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.ContentSearch.Linq" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.ContentSearch.ContentExtraction" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.Assemblies.Platform" Version="$(PlatformVersion)" /> <PackageReference Update="Sitecore.LayoutService.Client" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Sitecore.AspNet.RenderingEngine" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Sitecore.AspNet.ExperienceEditor" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Sitecore.AspNet.Tracking" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Sitecore.AspNet.Tracking.VisitorIdentification" Version="$(SitecoreAspNetVersion)" /> <PackageReference Update="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(Net6x)" /> <PackageReference Update="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(Net6x)" /> <PackageReference Update="Microsoft.Extensions.Http" Version="$(Net6x)" /> <PackageReference Update="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="$(Net6x)" /> </ItemGroup> </Project>
We start by adding some references to our renderinghost project
- Sitecore.LayoutService.Client
- Sitecore.AspNet.RenderingEngine
- Sitecore.AspNet.ExperienceEditor
- Sitecore.AspNet.Tracking
- Sitecore.AspNet.Tracking.VisitorIdentification


We create a new class “SitecoreOptions” in a folder called “Configuration” with following content
namespace MyDockerExperience.Rendering.Configuration { public class SitecoreOptions { public static readonly string Key = "Sitecore"; public Uri InstanceUri { get; set; } public string LayoutServicePath { get; set; } = "/sitecore/api/layout/render/jss"; public string DefaultSiteName { get; set; } public string ApiKey { get; set; } public Uri RenderingHostUri { get; set; } public bool EnableExperienceEditor { get; set; } public Uri LayoutServiceUri { get { if (InstanceUri == null) return null; return new Uri(InstanceUri, LayoutServicePath); } } } }
As we created an empty project we add necessary folders for MVC

Underneath “Model” we create a new class
ErrorViewModel.cs
using System; namespace MyDockerExperience.Rendering.Models { public class ErrorViewModel { public bool IsInvalidRequest { get; set; } } }
Underneath “Views” we add some default views
_ViewStart.cshtml
@{ Layout = "_Layout"; }
_ViewImports.cshtml
@using MyDockerExperience.Rendering.Models @using Sitecore.LayoutService.Client.Response.Model @using Sitecore.LayoutService.Client.Response.Model.Fields @using Sitecore.AspNet.RenderingEngine.Extensions @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @* This provides the sc-placeholder and field rendering tag helpers to our views. *@ @addTagHelper *, Sitecore.AspNet.RenderingEngine @* This provides the sc-visitor-identification tag helper. *@ @addTagHelper *, Sitecore.AspNet.Tracking.VisitorIdentification
Under Shared we add
_ComponentNotFound.cshtml
@model Component <h1>Unknown component '@Model?.Name'</h1>
_Layout.cshtml
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>@ViewData["Title"]</title> </head> <body> @RenderBody() </body> </html>
Error.cshtml
@model ErrorViewModel @{ ViewData["Title"] = "Error"; } <div class="column error-page"> <h1 class="text-danger">Error.</h1> <h2 class="text-danger">An error occurred while processing your request.</h2> @if (Model.IsInvalidRequest) { <div style="font-size: larger"> <strong>HINT:</strong> You probably need to publish your API key </div> <a></a> } </div>
NotFound.cshtml
<div class="column notfound-page"> <h1>Page Not Found</h1> </div>
Ok as we have now done the ground work, let’s create our first layout.
We create a new folder “Default” underneath “Views” and create an index.cshtml

In the index.html we add 3 Sitecore placeholders “header”, “main” & “footer”
<sc-placeholder name="header"></sc-placeholder> <sc-placeholder name="main"></sc-placeholder> <sc-placeholder name="footer"></sc-placeholder>
Underneath “Controller” we add a DefaultController which handles our requests
using Microsoft.AspNetCore.Mvc; using Sitecore.AspNet.RenderingEngine; using Sitecore.AspNet.RenderingEngine.Filters; using Sitecore.LayoutService.Client.Exceptions; using Sitecore.LayoutService.Client.Response.Model; using MyDockerExperience.Rendering.Models; using Microsoft.AspNetCore.Diagnostics; using System.Net; using Route = Sitecore.LayoutService.Client.Response.Model.Route; namespace MyDockerExperience.Rendering.Controllers { public class DefaultController : Controller { public DefaultController() { } // Inject Sitecore rendering middleware for this controller action // (enables model binding to Sitecore objects such as Route, // and causes requests to the Sitecore Layout Service for controller actions) [UseSitecoreRendering] public IActionResult Index(Route route) { var request = HttpContext.GetSitecoreRenderingContext(); if (request.Response.HasErrors) { foreach (var error in request.Response.Errors) { switch (error) { case ItemNotFoundSitecoreLayoutServiceClientException notFound: Response.StatusCode = (int)HttpStatusCode.NotFound; return View("NotFound", request.Response.Content.Sitecore.Context); case InvalidRequestSitecoreLayoutServiceClientException badRequest: case CouldNotContactSitecoreLayoutServiceClientException transportError: case InvalidResponseSitecoreLayoutServiceClientException serverError: default: throw error; } } } return View(route); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>(); return View(new ErrorViewModel { IsInvalidRequest = exceptionHandlerPathFeature?.Error is InvalidRequestSitecoreLayoutServiceClientException }); } } }
Next we will configure our launchsettings.json as following
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://0.0.0.0:8284", "sslPort": 44311 } }, "profiles": { "MyDockerExperience.Rendering": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "publishAllPorts": true, "useSSL": true } } }
To connect our Rendering host to the environment we need to adjust the appsettings.json
{ "Sitecore": { "InstanceUri": "https://cd.mydockerexperience.localhost", "LayoutServicePath": "/sitecore/api/layout/render/jss", "DefaultSiteName": "MyDockerExperience", "ApiKey": "YOUR KEY" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
We change our output pathes as following

Finally we add Directory.Build.props and a .dockerignore file to our root folder.
Directory.Build.props
“When MSBuild runs, Microsoft.Common.props searches your directory structure for the Directory.Build.props file (and Microsoft.Common.targets looks for Directory.Build.targets). If it finds one, it imports the file and reads the properties defined within it. Directory.Build.props is a user-defined file that provides customizations to projects under a directory.” (Source)
<Project> <!-- These props ensure there is no conflict between builds within your Rendering Host container ('dotnet watch'), and local builds on the container host (e.g. in Visual Studio). https://stackoverflow.com/a/60908066/201808 --> <PropertyGroup Condition="'$(UsingMicrosoftNETSdk)' == 'true'"> <DefaultItemExcludes>$(DefaultItemExcludes);$(MSBuildProjectDirectory)/obj/**/*</DefaultItemExcludes> <DefaultItemExcludes>$(DefaultItemExcludes);$(MSBuildProjectDirectory)/bin/**/*</DefaultItemExcludes> </PropertyGroup> <PropertyGroup Condition="'$(UsingMicrosoftNETSdk)' == 'true' AND '$(DOTNET_RUNNING_IN_CONTAINER)' == 'true'"> <BaseIntermediateOutputPath>$(MSBuildProjectDirectory)/obj/container/</BaseIntermediateOutputPath> <BaseOutputPath>$(MSBuildProjectDirectory)/bin/container/</BaseOutputPath> </PropertyGroup> <PropertyGroup Condition="'$(UsingMicrosoftNETSdk)' == 'true' AND '$(DOTNET_RUNNING_IN_CONTAINER)' != 'true'"> <BaseIntermediateOutputPath>$(MSBuildProjectDirectory)/obj/local/</BaseIntermediateOutputPath> <BaseOutputPath>$(MSBuildProjectDirectory)/bin/local/</BaseOutputPath> </PropertyGroup> </Project>
.dockerignore
“Before the docker CLI sends the context to the docker daemon, it looks for a file named .dockerignore
in the root directory of the context. If this file exists, the CLI modifies the context to exclude files and directories that match patterns in it. This helps to avoid unnecessarily sending large or sensitive files and directories to the daemon and potentially adding them to images using ADD
or COPY
.” (Source)
# folders .git .gitignore .vs .vscode .config .sitecore build docker packages */*/*/bin/ */*/*/obj/ */*/*/out/ # files *Dockerfile docker-compose* **/*.md *.ps1 **/*.yml **/*.module.json sitecore.json
Now we are ready to start our Sitecore build and run the instance
docker-compose down docker-compose build Run docker-compose up -d
Only a few steps left. To make SXA-JSS working with our rendering host we first need to add the Placeholder settings for our previously added Layout placeholders.

Next we need to create a custom layout and add these placeholders for the layout services

Finally we need to adjust the presentation details of on the template standard values for our project, as it is using the default layout and we want to use our custom layout.



We can now start editing our home item in Experience Editor

When we visit our Public Host, it is just blank as we have not yet placed any component.

Let’s add our first component and let’s see what happens.
Remember we configured SXA-JSS with all modules, so we should be able to add something into our experience editor.
First thing that we recognize: There is no drag & drop Toolbox

We do not need this, so we follow the second approach over the add component functionality


We notice that we can select SXA components and we will add an image to our main placeholder to see what happens.

We get an unknown component error as we have not registered any view or model in our rendering host.
Looking at the code for this view in SXA, we see that we can’t reuse it as it has dependencies to SXA which is not compatible with .NET Core yet
@using Sitecore.XA.Foundation.MarkupDecorator.Extensions @using Sitecore.XA.Foundation.RenderingVariants.Extensions @using Sitecore.XA.Foundation.SitecoreExtensions.Extensions @using Sitecore.XA.Foundation.Variants.Abstractions.Fields @model Sitecore.XA.Feature.Media.Models.ImageRenderingModel <div @Html.Sxa().Component(Model.Rendering.RenderingCssClass ?? "image", Model.Attributes)> <div class="component-content"> @if (Model.DataSourceItem != null) { foreach (BaseVariantField variantField in Model.VariantFields) { @Html.RenderingVariants().RenderVariant(variantField, Model.DataSourceItem, Model.RenderingWebEditingParams, Model.Href) } } else { @Model.MessageIsEmpty } </div> </div>
We have 2 possibilities. If we want to reuse the components of SXA we would to rewrite them in ASP.NET renderinghost or we just ignore these modules and create our own from the scratch.
We will take now the existing image and rewrite it but ignore variants and overhead. Our goal is just to display the image and make it editable.
In our rendering host project we create a view and a model for our image

First we have a look on the item in Sitecore


It has 3 fields “Image”(ImageField), “TargetUrl”(General Link) and “ImageCaption”(Single Line Text).
In the next step we will modify or model for this item, modify our view to support Experience Editor and configure our rendering host to bind these models to the request.
In our model we add following for the fields of the item.
using Sitecore.LayoutService.Client.Response.Model.Fields; namespace MyDockerExperience.Rendering.Models { public class ImageViewModel { public HyperLinkField TargetUrl { get; set; } public TextField ImageCaption { get; set; } public ImageField Image { get; set; } } }
Next we add following to our view, to make our model editable in Sitecore Experience Editor
@model ImageViewModel <sc:sc-text asp-for="ImageCaption"></sc:sc-text> <sc-link asp-for="TargetUrl"></sc-link> <sc-img asp-for="Image"></sc-img>
In the Program.cs of our rendering host project we add a ModelBoundView for our image model
builder.Services.AddSitecoreRenderingEngine(options => { //Register your components here options .AddModelBoundView<ImageViewModel>("Image") .AddDefaultPartialView("_ComponentNotFound"); })
Now we are able to edit the fields in Sitecore Experience Editor


At this point we will finish this walkthrough. The rest is pure development.
Conclusion
Working with Docker is fun and when you have faced the first obstacles and you know how to solve them you will be very effective. The learning curve is moderate and i would really suggest to everyone to have a deeper look into the materia.
When you have set up once an environment and make it configurable it is really easy to spin up new instances and try things out.
Following some Basics:
docker-compose/docker-compose.override
rolename(e.g.:mssql-init):
image: <Custom Image name> (e.g.: ${REGISTRY}${COMPOSE_PROJECT_NAME}-xm1-mssql-init:${VERSION:-latest}
build:
context: <Path to build> (e.g.: ./docker/build/mssql-init)
args:
PARAMETER_FOR_DOCKERFILE: <IMAGE> (e.g.: ${SITECORE_DOCKER_REGISTRY}sitecore-xp1-mssql-init:${SITECORE_VERSION})
volumes:
- Mount local folder to docker folder (e.g. ${LOCAL_DATA_PATH}\cd:C:\inetpub\wwwroot\App_Data\logs)
environment:
-VARIABLE: <VARIABLE_VALUE> (e.g. SITECORE_LICENSE_LOCATION: c:\license\license.xml)
Example content of a docker file
# escape=`
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
It seems that hotload functionality from .NET Core is causing issues at the moment, why it is from time to time needed to build the container again. (Hopefully this will be fixed soon)
We should add a seperate namespace to the images, otherwise when running a build an error stating “Status: max depth exceeded, Code: 1” may occur

First step: Create solution
Add Docker files & Create & Build custom images
The content of the docker file
# escape=`
ARG BASE_IMAGE
FROM ${BASE_IMAGE}