.NET 8.0+ local development environment
This guide describes how a partner runs Configured Commerce on .NET8+ locally, using the insite-commerce-cloud partner repository.
The backend runs as a set of services, and an nginx reverse proxy (running as a container) sits in front of everything to route a single browser domain to the correct service: the Spire storefront, the Admin/CMS APIs, the Storefront API, or the Integration API.
This document is Spire-focused. The Classic storefront path (classic-proxy) is noted where relevant but is not the primary workflow.
Prerequisites
Requirement | Notes |
|---|---|
Recent Configured Commerce Version | Be on 5.2.2604.725-lts or newer. |
Docker Desktop | Linux containers mode. Installation is out of scope here. |
.NET SDK | Required to build your `Extensions` project and (for Mode A) to run the APIs. Version `5.2.2605` and later target `net10.0`; `5.2.2512`–`5.2.2604` target `net8.0`. |
Node 22 | Required by Spire. Volta is recommended: `winget install -e --id Volta.Volta`. Volta auto-selects the version pinned in `src/FrontEnd/package.json`. |
Registry access | The compose files pull base images from `optimizelyb2bpublic.azurecr.io`. Authenticate with the credentials Optimizely provides: `docker login optimizelyb2bpublic.azurecr.io` (or `az acr login --name optimizelyb2bpublic` if you use the Azure CLI). |
A Configured Commerce database | Seeded into the SQL Server container — see [section 2](https://docs.developers.optimizely.com/configured-commerce/docs/net8-local-development-environment#2-seed-a-database). |
HOSTS file entries | `host.docker.internal` routes to your computers IP address. `localhost` routes to `127.0.0.1` |
Merge most recent code
Pull the most recent code from GitHub. When merging, note the changes in base for both the .sln and .csproj files. The .sln has moved to the root and now has references to projects for the Storefront API, Admin API, Integration, the Docker compose, and files at the root of the project.
The Extensions project supports targeting .NET 8.0+. Properties in the .csproj are conditionally included based on the target framework and must remain to build the project. Include or exclude third-party references based on the .NET versions they support. By default, it targets net48. Change the target framework based on your release version:
Release | TargetFramework value |
|---|---|
5.2.2512 – 5.2.2604 | net8.0 or net48;net8.0 |
5.2.2605 and later | net10.0 or net48;net10.0 |
InsiteCommerce.Web
InsiteCommerce.Web causes a build failure if the Extensions project targets net8.0 or net10.0. InsiteCommerce.Web supports net48 only. Remove the project reference to Extensions.csproj from InsiteCommerce.Web.csproj or delete the InsiteCommerce.Web folder.
Build extensions
You most likely need to make changes to your Extensions code before you are able to build it successfully. The .NET 8 framework article contains additional details on some of the common scenarios you might run see. You must build your Extensions project before you can run the code locally.
Getting Started
1. Overview — what runs where
A local environment is made of three parts:
- Infrastructure, in Docker — SQL Server, Elasticsearch, IronPDF, MailHog, plus the two nginx reverse proxies. These always run as containers (docker-compose.yml).
- The three .NET APIs — Admin, Storefront, and Integration. These can run either as Docker containers or as .NET processes on your host (from your IDE or dotnet run).
- Spire, on your host — the Spire dev server runs from src/FrontEnd via npm run start so you keep hot-reload and a debugger.
The nginx spire-proxy container is the single entry point at http://localhost:30000, and it routes each request to the correct service. The two diagrams below show the two backend modes. Only where the APIs run changes — the proxy, the routing rules, and Spire are identical.
Note the default setup has MAIN_HOST=host.docker.internal in your .env which is used by nginx to to properly route traffic to the appropriate place. Changing this to localhost will cause nginx to try to route traffic to itself and result in errors.
Mode A — APIs on the host
Run the API projects (src/Admin.Api, src/Storefront.Api, src/Integration.Api) from your IDE or dotnet run. Each loads the repo-root .env and listens on its *_PORT (30070 / 30040 / 30080) — exactly where the proxy looks (host.docker.internal:<port>). Best for active backend/extension development: full debugging and fast iteration.
flowchart LR
browser["Browser<br/>localhost:30000"]
subgraph host["Your host"]
spire["Spire dev server<br/>:30020"]
admin["Admin.Api<br/>:30070"]
storefront["Storefront.Api<br/>:30040"]
integration["Integration.Api<br/>:30080"]
end
subgraph docker["Docker"]
proxy["spire-proxy (nginx)<br/>:30000"]
infra["SQL Server · Elasticsearch<br/>IronPDF · MailHog"]
end
browser --> proxy
proxy -->|"/admin · /api/v1/admin"| admin
proxy -->|"/api /userfiles"| storefront
proxy -->|"/integration"| integration
proxy -->|"catch-all: storefront pages"| spire
admin --- infra
storefront --- infra
integration --- infraMode B — APIs in Docker
The apps Docker Compose profile builds and runs the APIs as containers. Simplest to start and closest to production; use it when you aren't changing backend code often.
flowchart LR
browser["Browser<br/>localhost:30000"]
subgraph host["Your host"]
spire["Spire dev server<br/>:30020"]
end
subgraph docker["Docker"]
proxy["spire-proxy (nginx)<br/>:30000"]
admin["admin-api<br/>:30070"]
storefront["storefront-api<br/>:30040"]
integration["integration-api<br/>:30080"]
infra["SQL Server · Elasticsearch<br/>IronPDF · MailHog"]
end
browser --> proxy
proxy -->|"/admin · /api/v1/admin"| admin
proxy -->|"/api /userfiles"| storefront
proxy -->|"/integration"| integration
proxy -->|"catch-all: storefront pages"| spire
admin --- infra
storefront --- infra
integration --- infraRouting rules live in .config/reverse-proxy/routes.yaml. The proxy image itself (optimizelyb2bpublic.azurecr.io/commerce/reverse-proxy) is pulled prebuilt — you do not build it. See section 6 for details.
2. Seed a database
Start SQL Server (and the rest of the infrastructure):
docker compose up -dThis starts mssql, elasticsearch, elasticsearchnext, ironpdfengine, mailhog, the two proxies, and the database-updater. SQL Server is published on localhost:1433 (user sa, password Password1).
- Create and seed the database. Connect to localhost,1433 (sa / Password1) with Azure Data Studio, SSMS, or sqlcmd, then:
- Create an empty database named Insite.Commerce.
- Against that database, run database/Insite.Commerce.StartingDatabase.sql (schema and base data).
- Optionally run database/Insite.Commerce.SampleData.sql to load the sample catalog and content.
- Apply version migrations. Re-run the stack so database-updater brings the seeded database up to the current version:
docker compose up -dThe database files live in the .sql/ volume on disk, so the database survives container restarts.
3. Configuration (.env)
Configuration is read from the repo-root .env file, which is already present in the partner repo. In Mode B the API projects load this same file at startup. The values that matter for local development:
Variable | Default | Purpose |
|---|---|---|
IMAGE_TAG | 5.2.2605.600-sts | Version of the prebuilt base images to pull. |
DB_CONNECTION_STRING | Server=host.docker.internal... | Main connection string. |
Developer__StartInternalWis | false | Set to true to use Admin.Api to run internal wis jobs |
Changing any of the following values is not recommended
Variable | Default | Purpose |
|---|---|---|
SPIRE_PORT | 30000 | Port the spire-proxy (your main entry point) listens on. |
SPIRE_WEB_PORT | 30020 | Port the Spire dev server's ingress listener uses (the proxy forwards storefront pages here). |
STOREFRONT_API_PORT | 30040 | Storefront API (container or host process). |
ADMIN_API_PORT | 30070 | Admin/CMS/Identity API (container or host process). |
INTEGRATION_API_PORT | 30080 | Integration API (container or host process). |
CLASSIC_PORT | 30100 | The Classic storefront proxy (not the primary path for Spire). |
MAIN_HOST | host.docker.internal | How the proxy containers reach services published on your host. If this is set to localhost and nginx is running in docker then nginx will route traffic to itself and result in errors |
4. Start the backend APIs
Pick one of the two modes from section 1. Both rely on the infrastructure and proxies started in section 3 (docker compose up -d).
Mode A — on the host
Leave the apps profile stopped and run the API projects directly from your IDE (set them as startup projects) or from a terminal:
dotnet run --project src/Storefront.Api
dotnet run --project src/Admin.Api
dotnet run --project src/Integration.Api # optional - only if you need to run wis jobsEach project calls AppHost.RunAsync(..., o => o.LoadDotEnvFile = true), so it reads the repo-root .env for the connection string, ports, and other settings, and listens on its *_PORT value. Because the proxy already targets host.docker.internal:<*_PORT>, no extra wiring is needed — the proxy routes straight to your host processes.
Mode B — in Docker
The three APIs are gated behind the apps Docker Compose profile. Start (and build) them with:
docker compose --profile apps up -d --buildWhat the build does (per src/<App>.Api/Dockerfile): it pulls the prebuilt optimizelyb2bpublic.azurecr.io/commerce/webapp base image, compiles only your Extensions.csproj for net10.0, and overlays the resulting Extensions.dll onto the base image. Each container then runs as Admin, Storefront, or Integration via its HOST_TYPE.
- You do not build the platform itself — only your extensions.
- Rebuild the images (--build) whenever you change extension code, or rebuild just the affected service, e.g. docker compose --profile apps up -d --build admin-api.
5. The nginx routing layer
This is the part that is new compared with a net48 setup, and the reason Docker is required even when the APIs and Spire run on your host.
Both proxies (classic-proxy, spire-proxy) use the same prebuilt nginx image and the same route map, mounted from .config/reverse-proxy/routes.yaml. They differ only in two environment variables that decide where the catch-all goes:
Proxy | Port | FRONTEND | CONTENT_ADMIN | Catch-all goes to |
|---|---|---|---|---|
spire-proxy | 30000 | spire | spire | The Spire dev server (SPIRE_WEB_HOST:SPIRE_WEB_PORT). |
classic-proxy | 30100 | storefront-api | admin-api | The Classic storefront (Storefront API). |
routes.yaml maps specific path prefixes to specific services; anything not matched falls through to the catch-all. The important mappings:
- /admin, /api/internal, /api/v1/admin, /identity/admin, /identity/connect, /ckfinder, /systemresources → admin-api
- /api, /identity, /account/isauthenticated, /email, /Excel, /userfiles, /sitemap, *.txt → storefront-api
- /integration → integration-api
- everything else (the storefront pages) → the catch-all (Spire, for spire-proxy)
The net effect: the browser only ever talks to http://localhost:30000, and nginx fans the request out to the correct service — whether it's a container or a host process. This mirrors how traffic is routed in a real (Kubernetes) deployment.
6. Start Spire
With the backend running (Mode A or Mode B), start the Spire dev server on your host:
cd src/FrontEnd
npm install
npm run startnpm run start starts two listeners. This is intentional and allows a single spire instance to be used for net48 along with net8+:
- Port 3000 — the standalone Spire dev server. It sends API calls to the URL in src/FrontEnd/config/settings.js (default http://localhost:3010). Typically only used for a net48 setup.
- Port SPIRE_WEB_PORT (30020) — The nginxspire-proxy sends traffic to this listener. It starts automatically when startDevelopment.js finds the repo-root .env. When spire makes api calls to dotnet using this listener it will send those api calls back to spire-proxy and ignore the values in settings.js
For the integrated local stack, use the proxy entry point — http://localhost:30000 — not http://localhost:3000.
7. Access and verify
What | URL |
|---|---|
Storefront (Spire) | |
Admin Console | |
MailHog (outgoing mail) | |
Classic storefront (optional) |
Sign-in and the Admin Console rely on the Identity endpoints served by the Admin API (OIDC_AUTHORITY=http://host.docker.internal:30070), routed through the proxy under /identity.
If the storefront has no pages on first load, Spire generates them on the first request. To force regeneration, run DELETE FROM content.Node against the database and reload.
8. Reference
Containers
Service | Image / build | Default port(s) | Notes |
|---|---|---|---|
mssql | mcr.microsoft.com/mssql/server:2019 | 1433 | sa / Password1. Data persisted in .sql/. |
elasticsearch | Elasticsearch 5.6 | 9201 | |
elasticsearchnext | Elasticsearch 7.10 | 9200 | |
ironpdfengine | IronPDF | 33350 | PDF generation. |
mailhog | MailHog | 2525 (SMTP), 8025 (UI) | Captures outgoing mail. |
database-updater | built from src/Database.Updater | — | Runs DB migration scripts, then exits. |
spire-proxy | …/commerce/reverse-proxy | 30000 | nginx ingress for Spire. Main entry point. |
classic-proxy | …/commerce/reverse-proxy | 30100 | nginx ingress for Classic. |
admin-api | webapp base + your Extensions | 30070 | apps profile (Mode A). Admin / CMS / Identity. |
storefront-api | webapp base + your Extensions | 30040 | apps profile (Mode A). |
integration-api | webapp base + your Extensions | 30080 | apps profile (Mode A). Runs internal WIS jobs. |
In Mode B the last three run as host processes (src/Admin.Api, src/Storefront.Api, src/Integration.Api) on the same ports instead of as containers.
9. Troubleshooting
- database-updater fails on first run — the Insite.Commerce database doesn't exist yet. Seed it (section 3), then re-run docker compose up -d.
- Exceptions aren't in the ApplicationLog table — with ASPNETCORE_ENVIRONMENT=Development (the default), unhandled exceptions are written to the console instead. Check the container logs (docker compose logs -f <service>) in Mode A, or the IDE/terminal output in Mode B.
- docker compose can't pull images / 401 Unauthorized — authenticate to the registry: docker login optimizelyb2bpublic.azurecr.io (or az acr login --name optimizelyb2bpublic).
- Storefront loads but API calls fail / you see the Spire dev server, not the integrated site — make sure you're browsing http://localhost:30000 (the proxy), not http://localhost:3000 (the standalone Spire listener).
- Proxy returns 502 in Mode B — the API process isn't running or isn't listening on the expected *_PORT. Confirm the API started and that it loaded .env (it logs the port on startup).
- Port already in use — another checkout or process is using one of the published ports. Change the ports in .env (section 4) and restart the affected pieces.
- Extension build errors — your Extensions project must build for the netcore target (net10.0 for 5.2.2605+). In Mode A the Docker build forces this; in Mode B set ExtensionsTargetFrameworks so the API project's reference resolves.