You have Sitecore 10.4 running in Docker containers with SXA. Everything looks fine. But when you try to add a rendering and look for JSON Rendering — it’s simply not there. The rendering type doesn’t exist in your Available Renderings.
If this sounds familiar, here’s exactly what’s going on and how to fix it — step by step.
What’s Going On?
JSON Rendering is part of Sitecore Headless Services (formerly JSS Server Components). If your Docker setup was built with just SPE and SXA modules, Headless Services isn’t included. You need to add it.
The good news: you don’t need a new container or a fresh setup. You layer the Headless Services module on top of your existing CM image using your Dockerfile. That’s it.
Here’s what the layering looks like:
Base CM Image → + SPE → + SXA → + Headless Services → Your Custom CM
Important: Only the CM image needs Headless Services. Unlike some modules, Headless Services does NOT have database DACPACS or Solr configs in its asset image for Sitecore 10.4 — those DB changes are already pre-baked into the base mssql-init image. You only modify the CM Dockerfile.
Prerequisites
- Sitecore 10.4 running in Docker with SXA (XP0 topology)
- Docker Desktop for Windows with Windows Containers enabled
- Sitecore license with Headless Services entitlement
- Your project’s docker-compose files and Dockerfiles accessible
Check Your License First
Before doing anything, verify your license includes Headless Services. Save the following as Check-SitecoreHeadlessLicense.ps1 and run it:
# Check-SitecoreHeadlessLicense.ps1
# Usage: .\Check-SitecoreHeadlessLicense.ps1 -LicensePath "<your location>\license.xml"
param(
[Parameter(Mandatory=$true)]
[string]$LicensePath
)
if (-not (Test-Path $LicensePath)) {
Write-Host "ERROR: License file not found at: $LicensePath" -ForegroundColor Red
exit 1
}
$content = Get-Content $LicensePath -Raw
Write-Host ""
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " Sitecore Headless License Check" -ForegroundColor Cyan
Write-Host "==========================================" -ForegroundColor Cyan
Write-Host " License file: $LicensePath"
Write-Host ""
if ($content -match "Sitecore\.JSS") {
Write-Host " [PASS] Sitecore.JSS key found" -ForegroundColor Green
Write-Host " Your license includes Headless Services." -ForegroundColor Green
} else {
Write-Host " [FAIL] Sitecore.JSS key NOT found" -ForegroundColor Red
Write-Host " Your license does not include Headless Services." -ForegroundColor Red
Write-Host " Contact your Sitecore account manager to obtain" -ForegroundColor Yellow
Write-Host " a license with the JSS/Headless entitlement." -ForegroundColor Yellow
}
# Bonus: check other modules
Write-Host ""
Write-Host " Other module entitlements found:" -ForegroundColor Cyan
$modules = @(
@{ Key = "Sitecore.SPE"; Label = "PowerShell Extensions (SPE)" },
@{ Key = "Sitecore.SXA"; Label = "Experience Accelerator (SXA)" },
@{ Key = "Sitecore.Horizon"; Label = "Horizon (Pages)" },
@{ Key = "Sitecore.Publishing"; Label = "Publishing Service" }
)
foreach ($mod in $modules) {
if ($content -match [regex]::Escape($mod.Key)) {
Write-Host " [*] $($mod.Label)" -ForegroundColor DarkGreen
}
}
Write-Host ""
Run it like this:
.\Check-SitecoreHeadlessLicense.ps1 -LicensePath "<your location>\license.xml"
If you see [PASS], you’re good to proceed. Sitecore 10.2+ subscription licenses include Headless Services automatically.
Files You’ll Modify
Only three files:
your-project/
docker/build/cm/
Dockerfile ← add Headless Services layer
docker-compose.override.yml ← add build arg
.env ← add version variable
Step 1: Update Your .env File
Open your .env file and add the HEADLESS_VERSION variable alongside your existing module versions:
# Existing module versions
SPE_VERSION=6.5-1809
SXA_VERSION=10.4-1809
# Add this line
HEADLESS_VERSION=22.0-1809
Note: The tag depends on your Windows base image. Use
1809for ltsc2019 orltsc2022for Server 2022. Check the Sitecore Docker Images repo on GitHub for the exact tag for your version.
Step 2: Update docker-compose.override.yml
In your docker-compose.override.yml, add HEADLESS_SERVICES_IMAGE to the cm service’s build args. The new line is highlighted with a comment:
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}
SOLUTION_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest}
# --- ADD THIS LINE ---
HEADLESS_SERVICES_IMAGE: ${SITECORE_MODULE_REGISTRY}sitecore-headless-services-xp1-assets:${HEADLESS_VERSION}
Step 3: Update the CM Dockerfile
Open docker/build/cm/Dockerfile and make two additions.
3a. Add the ARG and FROM at the top
Add HEADLESS_SERVICES_IMAGE to your ARGs, and add its FROM stage before FROM ${BASE_IMAGE}. The new lines are marked with comments:
# escape=`
ARG BASE_IMAGE
ARG SXA_IMAGE
ARG SPE_IMAGE
ARG HEADLESS_SERVICES_IMAGE # <--- ADD THIS
ARG TOOLING_IMAGE
ARG SOLUTION_IMAGE
FROM ${SOLUTION_IMAGE} as solution
FROM ${TOOLING_IMAGE} as tooling
FROM ${SPE_IMAGE} as spe
FROM ${SXA_IMAGE} as sxa
FROM ${HEADLESS_SERVICES_IMAGE} AS headless_services # <--- ADD THIS
FROM ${BASE_IMAGE}
🚨 Critical:
FROM ${BASE_IMAGE}must always be last. The last unnamed FROM becomes your final output image. If you place another FROM after it, your image will be built on the wrong base. This is a very common mistake.
3b. Add the COPY and RUN after the SXA block
After your existing SXA module section, add the Headless Services layer:
# Add SPE module
COPY --from=spe \module\cm\content .\
# Add SXA module
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;
# --- ADD THIS BLOCK ---
# 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 artifacts
COPY --from=solution \artifacts\cm\ C:\inetpub\wwwroot
Step 4: Build and Deploy
Stop your current containers:
docker compose stop
Build the updated images:
docker compose build
This pulls the Headless Services asset image and layers it onto your CM image. The first build takes a few minutes for the download.
Start the environment:
docker compose up --detach
Wait for healthy status:
docker compose ps
Wait until all containers show healthy before proceeding.
Step 5: Verify the Installation
Once the environment is up and healthy:
1. Test the Layout Service API
Browse to:
https://<your-cm-host>/sitecore/api/layout/render/jss?item=/&sc_apikey=TEST
You should get HTTP 400 with the message “API key is not valid.” This confirms the Headless Services API is responding. A 404 would mean the module isn’t installed.
2. Check Content Editor
Navigate to /sitecore/system/Modules/Layout Service in the Content Editor. If this node exists, the items have been deployed correctly.
3. Find JSON Rendering
Create a new rendering — JSON Rendering should now appear as an available rendering type. This is what we came here for!
4. Rebuild Indexes
Go to Control Panel → Populate Solr Managed Schema → Select All → Populate. Then Indexing Manager → Select All → Rebuild.
Gotchas and Lessons Learned
I ran into several issues getting this working. Here’s what I learned so you don’t have to.
🚫 Don’t add Headless Services to mssql-init
This is the biggest trap. The Headless Services asset image does not contain a C:\module\db folder. If you add a COPY instruction for that path in your mssql-init Dockerfile, the build fails with:
COPY failed: stat C:\module\db: file does not exist
For Sitecore 10.4, Headless Services database changes are pre-baked into the base mssql-init image. Only the CM Dockerfile needs the Headless Services layer.
🚫 Don’t assume all asset images have the same folders
Each module’s asset image may contain different combinations of these folders:
C:\module\
cm\content\ ← CM webroot files (not all modules have this)
cd\content\ ← CD webroot files (not all modules have this)
db\ ← dacpac files (not all modules have this)
solr\ ← Solr configs (not all modules have this)
tools\ ← Initialize scripts (not all modules have this)
Always inspect before you COPY. Run this to see what’s actually inside an asset image:
docker run -it <asset-image-name> powershell
dir C:\module\
🚫 Watch your FROM order in Dockerfiles
The last unnamed FROM becomes your final output image. This is wrong:
FROM ${SPE_IMAGE} as spe
FROM ${BASE_IMAGE} # NOT the final image!
FROM ${HEADLESS_IMAGE} as hds # This becomes your final image - WRONG
This is correct:
FROM ${SPE_IMAGE} as spe
FROM ${HEADLESS_IMAGE} as hds # Named stage, used for COPY only
FROM ${BASE_IMAGE} # Last = your final image - CORRECT
🚫 Keep SXA image names consistent
If your docker-compose.override.yml uses sxa-xp1-assets in one service and sitecore-sxa-xp1-assets in another, you’ll pull different images or get build failures. Use the same image name in every service.
Summary
Three files changed, one docker compose build, and your JSON Rendering is available:
| File | Change |
|---|---|
.env |
Add HEADLESS_VERSION variable |
docker-compose.override.yml |
Add HEADLESS_SERVICES_IMAGE build arg to the cm service |
docker/build/cm/Dockerfile |
Add ARG, FROM, COPY, and RUN for headless_services |
No need to start from scratch or spin up a new container environment. Happy coding!