.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.2512 or newer. |
Docker Desktop | Linux containers mode. Installation is out of scope here. |
.NET SDK | Required to build your |
Node 22 | Required by Spire. Volta is recommended: |
Registry access | The compose files pull base images from |
A Configured Commerce database | Seeded into the SQL Server container — see section 2. |
HOSTS file entries |
|
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/FrontEndvianpm run startso 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 --- infra
Mode 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 --- infra
In both modes the proxy reaches the APIs and Spire through
host.docker.internal:<port>, so the only thing that changes between modes is whether each API process lives inside Docker or on your host.
Routing 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).
On the very first run,
database-updaterwill fail because theInsite.Commercedatabase does not exist yet. That is expected — seed the database (next step), then re-rundocker compose up -d.
-
Create and seed the database. Connect to
localhost,1433(sa/Password1) with Azure Data Studio, SSMS, orsqlcmd, 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.sqlto load the sample catalog and content.
- Create an empty database named
-
Apply version migrations. Re-run the stack so
database-updaterbrings 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)
.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.
Extensions target framework: the API projects target the netcore TFM (
net10.0for5.2.2605+), so yourExtensionsproject must build for that target too.Extensions.csprojdefaults tonet48; set theExtensionsTargetFrameworksMSBuild property (e.g. inDirectory.Build.propsor via-p:ExtensionsTargetFrameworks=net10.0) so the reference resolves.
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)
Routes are matched most-specific first, so
/admin/account/impersonate(storefront-api) resolves before/admin(admin-api).
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(defaulthttp://localhost:3010). Typically only used for a net48 setup. - Port
SPIRE_WEB_PORT(30020) — The nginxspire-proxysends traffic to this listener. It starts automatically whenstartDevelopment.jsfinds the repo-root.env. When spire makes api calls to dotnet using this listener it will send those api calls back tospire-proxyand ignore the values in settings.js
For the integrated local stack, use the proxy entry point — http://localhost:30000 —
not http://localhost:3000.
src/FrontEnd/config/settings.jsis created automatically (copied fromsettings-base.js) on first run and is gitignored, so you can change the standalone API URL without affecting the repo.
7. Access and verify
| What | URL |
|---|---|
| Storefront (Spire) | http://localhost:30000 |
| Admin Console | http://localhost:30000/admin |
| MailHog (outgoing mail) | http://localhost:8025 |
| Classic storefront (optional) | http://localhost:30100 |
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-updaterfails on first run — theInsite.Commercedatabase doesn't exist yet. Seed it (section 3), then re-rundocker compose up -d. -
Exceptions aren't in the
ApplicationLogtable — withASPNETCORE_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 composecan't pull images / 401 Unauthorized — authenticate to the registry:docker login optimizelyb2bpublic.azurecr.io(oraz 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), nothttp://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
Extensionsproject must build for the netcore target (net10.0for5.2.2605+). In Mode A the Docker build forces this; in Mode B setExtensionsTargetFrameworksso the API project's reference resolves.
