# Local Development Environment for SAP Commerce Cloud: Full CCv2 Parity

> A CCv2-aligned local SAP Commerce setup with SQL Server in Docker, the correct JDK, manifest reuse, and cloud constraints.

Source: https://www.commercecloudintegrations.com/guides/local-development-environment-for-sap-commerce
Publisher: Commerce Cloud Integrations
Updated: 2026-09-30
Topics: development, ccv2, local-setup, mssql, manifest, tooling

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 since August 31, 2026 |
| SAP Commerce Cloud 2211 (JDK 17) | up to the August 2025 update | Out of maintenance since the 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:

```bash
# 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 overdue: since August 31, 2026, Commerce Cloud no longer builds JDK 17 code. Treat it as the current sprint 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](https://github.com/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:

1. **The platform itself is not in Git.** Only `hybris/bin/custom` and configuration. The cloud builder downloads the platform matching `commerceSuiteVersion`; locally you unzip the same artifact from the SAP Download Center into the same folder structure.
2. **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:

```bash
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:

```sql
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):

```bash
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:

```sql
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`:

```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:

```json
{
  "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 `production` persona overrides the common file in production builds only. You stop maintaining parallel property sets that drift.
- **extensions**: the cloud builds from the same `localextensions.xml` you run locally. The `exclude` list removes purely local extensions (`hac` is 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.xml` and 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:

```xml
<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 customize` is 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:

```properties
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:

1. **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.
2. **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:

```json
{
  "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.x` means JDK 21, SapMachine)
- [ ] SQL Server 2022 in Docker with `READ_COMMITTED_SNAPSHOT` and `ALLOW_SNAPSHOT_ISOLATION` on
- [ ] `manifest.json` uses `useConfig` for properties, extensions, Solr, and languages
- [ ] `localextensions.xml` is shared between local and cloud, with local-only extensions excluded
- [ ] Third-party JARs resolved via `external-dependencies.xml`, not committed
- [ ] `session.serialization.check=true` in 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.
