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

  1. Sitecore 10 with Docker – Create a solution from the scratch – Setting up the first docker instance
  2. Sitecore 10 with Docker – Create a solution from the scratch – Setup Sitecore 10.3 XM setup with custom images
  3. Sitecore 10 with Docker – Create a solution from the scratch – Add Modules and enable Sitecore Serialization
  4. Sitecore 10 with Docker – Create a solution from the scratch – Create solution and enable Sitecore Serialization
  5. 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.

  1. Preparing the urls, certificates, & init.ps1
  2. Configuring the CM to communicate with the rendering host
  3. 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}

Leave a Comment

Your email address will not be published. Required fields are marked *