Local Development Environment for SAP Commerce Cloud: Full CCv2 Parity
Build a local SAP Commerce development setup that mirrors CCv2: SQL Server in Docker, correct JDK track, manifest-driven configuration reuse, and the cloud constraints you must respect from day one.
Most CCv2 build failures and "works on my machine" incidents trace back to the same root cause: the local environment drifts from what the cloud actually runs. The cloud builder assembles your platform from manifest.json, runs against Azure SQL, pins a specific Solr version, and refuses several things developers rely on locally (looking at you, ant customize). If your laptop setup does not reproduce those constraints, you discover them one failed cloud build at a time, at roughly 40 minutes per attempt.
This guide sets up a local environment that behaves like CCv2: same database engine, same JDK track, same configuration sources, same extension list. It replaces two old CX Works articles (local development practices and the local SQL Server install) and updates them for the 2211 update-release world.
The Version Matrix First#
Before installing anything, pin your versions. On 2211 the platform is delivered as update releases, and since the JDK 21 framework update there are two tracks:
| Component | Track | Notes |
|---|---|---|
| SAP Commerce Cloud 2211 (JDK 21) | 2211-jdk21.x | Current track. Requires JDK 21 and brings Spring 6. Mandatory adoption by August 31, 2026 |
| SAP Commerce Cloud 2211 (JDK 17) | up to the August 2025 update | Maintained only until end of June 2026. Do not start new work here |
| Integration Extension Pack | own release cycle | Must match your commerce suite version; check the compatibility matrix on SAP Help |
| Composable storefront | own release cycle | Versioned separately from the platform, consumed via OCC |
| Solr | pinned per update release | The cloud runs a specific Solr version per commerce version; mirror it locally |
Use SapMachine for the JDK. It is what SAP supports and what the cloud builder uses:
# Verify what you are actually running before blaming the platform
java -version
# openjdk version "21.0.x" ... SapMachine
If your team is still on the JDK 17 track, the migration to 2211-jdk21 is not optional and the deadline is not soft. Treat it as a current sprint item, not a backlog item.
Repository Layout: Mirror the Cloud Builder#
CCv2 clones your Git repository and builds from core-customize/manifest.json. The layout that the cloud expects, and that you should use locally, follows the SAP-samples/cloud-commerce-sample-setup structure:
repository-root/
├── core-customize/
│ ├── manifest.json
│ └── hybris/
│ ├── bin/custom/ # your extensions only
│ └── config/ # local dev config, never secrets
│ ├── local.properties
│ ├── localextensions.xml
│ └── environments/
│ ├── local-dev.properties
│ ├── d1.properties
│ ├── s1.properties
│ └── p1.properties
├── js-storefront/ # composable storefront, if used
└── _LANGUAGES_/ # language pack zips, if used
Two rules that save real pain:
- The platform itself is not in Git. Only
hybris/bin/customand configuration. The cloud builder downloads the platform matchingcommerceSuiteVersion; locally you unzip the same artifact from the SAP Download Center into the same folder structure. - No secrets in the repository, ever. Passwords, API keys, and client secrets belong in the Cloud Portal service configuration (per endpoint, per environment). Locally they live in
local.properties, which should carry only developer defaults.
Local Database: SQL Server in Docker#
CCv2 runs on Azure SQL Database. HSQLDB, the platform default for quick demos, has different locking behaviour, different type handling, and different SQL dialect quirks. MySQL is closer but still not the same engine. Developing against anything other than SQL Server means your FlexibleSearch edge cases, DDL statements, and multi-threaded ImpEx imports are tested for the first time in the cloud.
The fix takes five minutes with Docker. The old microsoft/mssql-server-linux image from the original CX Works article is dead; use the current Microsoft Container Registry image:
docker run -d --name commerce-mssql \
-e "ACCEPT_EULA=Y" \
-e "MSSQL_SA_PASSWORD=Local-Dev-Passw0rd!" \
-p 1433:1433 \
-v commerce-mssql-data:/var/opt/mssql \
mcr.microsoft.com/mssql/server:2022-latest
The named volume matters. Without it, every container recreation wipes your initialized system, and a full ant initialize on a realistic catalog is not something you want to repeat because Docker Desktop restarted.
Create the database and set the isolation parameters SAP Commerce expects:
CREATE DATABASE commerce;
GO
ALTER DATABASE commerce SET READ_COMMITTED_SNAPSHOT ON;
ALTER DATABASE commerce SET ALLOW_SNAPSHOT_ISOLATION ON;
GO
You can run these through sqlcmd inside the container (the modern tools path is mssql-tools18, and -C trusts the self-signed certificate):
docker exec -it commerce-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'Local-Dev-Passw0rd!' -C \
-Q "CREATE DATABASE commerce"
Skip the snapshot isolation settings and the platform will warn you at startup, in block capitals, that multi-threaded ImpEx imports may deadlock:
WARN [main] [hybrisserver] ************ Parameter READ_COMMITTED_SNAPSHOT is turned off!! ************
WARN [main] [hybrisserver] ** Without this parameter set if you are using ImpEx to run multi-threaded **
WARN [main] [hybrisserver] ** imports, you may experience deadlocks. ...
Azure SQL is not identical to a boxed SQL Server, and the difference is expressed through the compatibility level. A new Azure SQL database currently defaults to compatibility level 160, which corresponds to SQL Server 2022, hence the image tag above. Check what your actual cloud environments run and match it:
SELECT name, compatibility_level FROM sys.databases;
Do not trust the version shown in HAC under Monitoring, Database. It reports the engine version and ignores the compatibility level, which is what actually governs query behaviour.
Wire it into local.properties:
db.url=jdbc:sqlserver://localhost:1433;databaseName=commerce;encrypt=false;responseBuffering=adaptive;loginTimeout=10
db.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
db.username=sa
db.password=Local-Dev-Passw0rd!
db.tableprefix=
Configuration Reuse: One Source of Truth in manifest.json#
The single most effective habit for CCv2 parity is to make manifest.json reference the same configuration files you use locally, via the useConfig block:
{
"commerceSuiteVersion": "2211",
"useConfig": {
"properties": [
{
"location": "hybris/config/environments/common.properties"
},
{
"location": "hybris/config/environments/p1.properties",
"persona": "production"
},
{
"location": "hybris/config/environments/accstorefront.properties",
"aspect": "accstorefront"
}
],
"extensions": {
"location": "hybris/config/localextensions.xml",
"exclude": ["hac"]
},
"solr": {
"location": "solr"
},
"languages": {
"location": "_LANGUAGES_"
}
}
}
What each block buys you:
- properties: layered per persona (environment type) and per aspect. The merge order is deterministic, so a property defined for the
productionpersona overrides the common file in production builds only. You stop maintaining parallel property sets that drift. - extensions: the cloud builds from the same
localextensions.xmlyou run locally. Theexcludelist removes purely local extensions (hacis the classic example: available locally, not permitted on CCv2 storefront aspects). If the lists differ in any other way, you have two platforms, not one. - solr: your
solrconfig.xmland schema customizations ship with the build instead of being hand-applied. Any Solr customization must match the Solr version the cloud pins for your commerce version, so mirror that version locally. - languages: only bundle language packs you actually support. Every additional language pack inflates initialization and build time for nothing.
Not every extension that exists in the platform distribution is allowed on CCv2. Before adding anything to localextensions.xml, check it against the compatibility notes on SAP Help. The build will not always fail fast on an unsupported extension; sometimes it fails at deployment, which is a far more expensive place to learn.
Third-Party Libraries: Maven, Not Committed JARs#
Committing JAR files into lib/ folders bloats the repository and turns every library upgrade into a manual chore. Each extension can instead declare dependencies in an external-dependencies.xml (a standard Maven POM), which both the local build and the cloud builder resolve at build time:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme.commerce</groupId>
<artifactId>acmecore</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.9</version>
</dependency>
</dependencies>
</project>
Enable it in the extension's extensioninfo.xml with usemaven="true". Keep the dependency list short and audited; every transitive JAR you pull in is something the platform classloader has to live with.
What CCv2 Will Not Let You Do#
Learn these constraints locally before the cloud teaches them to you:
ant customizeis not supported. If you believe you need to patch platform files, you almost always have an extension, an interceptor, a Spring bean override, or an AOP route instead. Genuine platform bugs go to SAP support, not to a patched JAR in your repository.- Sticky sessions with failover. The cluster binds sessions to nodes but backs them up for failover. Everything you put into the HTTP session must be serializable. Catch violations locally by enabling:
session.serialization.check=true
Run with this on in every developer instance. A non-serializable session attribute that slips through will surface in the cloud as users losing carts during a rolling deployment, which is a much worse way to find out.
- No filesystem persistence. Anything written to local disk on a node is gone after redeployment. Media goes to media storage, files to hot folders or blob storage, never to
${HYBRIS_DATA_DIR}paths you invented.
Monitoring Your Local Build Like the Cloud Does#
CCv2 ships Dynatrace agents on every node and a centralized logging endpoint (OpenSearch Dashboards, still widely called Kibana) per environment. Two habits translate well to local development:
- Structured, greppable logging. The cloud log pipeline indexes level, logger, and message. Log lines assembled from string concatenation with no consistent fields are unsearchable both locally and in the cloud.
- Alert on log patterns, not on outages. In the cloud you can define monitors against the log indices. A worked example: alerting to Slack whenever the storefront logs the classic populator misconfiguration warning. The extraction query filters on aspect, level, and message within a rolling window:
{
"query": {
"bool": {
"must": [
{ "match": { "kubernetes.labels.ccv2_cx_sap_com/platform-aspect": "accstorefront" } },
{ "match": { "logs.level": "WARN" } },
{ "match": { "logs.message": "No populator configured for option [VARIANT_FIRST_VARIANT]" } },
{ "range": { "@fb_timestamp": { "gte": "now-5m" } } }
]
}
}
}
The trigger condition is a one-line Painless script:
ctx.results[0].hits.total.value > 0
Wire the action to a Slack webhook destination and template the message with Mustache variables ({{ctx.monitor.name}}, {{ctx.trigger.severity}}, {{ctx.periodStart}}). If test messages never arrive, the usual culprit is a missing network policy for outbound traffic from the logging stack; that is a support ticket, not a configuration error on your side.
The point of setting this up early: your team learns which log patterns matter while still in development, so production alerting on day one is a copy of monitors you already trust.
Developer Loop Checklist#
A local environment has parity when all of these hold:
- JDK matches the cloud track (
2211-jdk21.xmeans JDK 21, SapMachine) - SQL Server 2022 in Docker with
READ_COMMITTED_SNAPSHOTandALLOW_SNAPSHOT_ISOLATIONon -
manifest.jsonusesuseConfigfor properties, extensions, Solr, and languages -
localextensions.xmlis shared between local and cloud, with local-only extensions excluded - Third-party JARs resolved via
external-dependencies.xml, not committed -
session.serialization.check=truein every developer instance - Solr version locally matches the version pinned by your commerce update release
- No secrets in Git; cloud secrets live in Cloud Portal service configuration
With this in place, the cloud build becomes a formality instead of a nightly gamble, and the next guide in this series, on the first CCv2 build and deployment, gets much shorter.