Dev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

.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 Extensions project and (for Mode A) to run the APIs.
Version 5.2.2605 and later target net10.0; 5.2.25125.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.

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:

ReleaseTargetFramework value
5.2.2512 – 5.2.2604net8.0 or net48;net8.0
5.2.2605 and laternet10.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:

  1. Infrastructure, in Docker — SQL Server, Elasticsearch, IronPDF, MailHog, plus the two nginx reverse proxies. These always run as containers (docker-compose.yml).
  2. 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).
  3. 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 --- 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 -d

This 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-updater will fail because the Insite.Commerce database does not exist yet. That is expected — seed the database (next step), then re-run docker compose up -d.

  1. Create and seed the database. Connect to localhost,1433 (sa / Password1) with Azure Data Studio, SSMS, or sqlcmd, then:

    1. Create an empty database named Insite.Commerce.
    2. Against that database, run database/Insite.Commerce.StartingDatabase.sql (schema and base data).
    3. Optionally run database/Insite.Commerce.SampleData.sql to load the sample catalog and content.
  2. Apply version migrations. Re-run the stack so database-updater brings the seeded database up to the current version:

    docker compose up -d

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

VariableDefaultPurpose
IMAGE_TAG5.2.2605.600-stsVersion of the prebuilt base images to pull.
DB_CONNECTION_STRINGServer=host.docker.internal...Main connection string.
Developer__StartInternalWisfalseSet to true to use Admin.Api to run internal wis jobs

Changing any of the following values is not recommended

VariableDefaultPurpose
SPIRE_PORT30000Port the spire-proxy (your main entry point) listens on.
SPIRE_WEB_PORT30020Port the Spire dev server's ingress listener uses (the proxy forwards storefront pages here).
STOREFRONT_API_PORT30040Storefront API (container or host process).
ADMIN_API_PORT30070Admin/CMS/Identity API (container or host process).
INTEGRATION_API_PORT30080Integration API (container or host process).
CLASSIC_PORT30100The Classic storefront proxy (not the primary path for Spire).
MAIN_HOSThost.docker.internalHow 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 jobs

Each 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.0 for 5.2.2605+), so your Extensions project must build for that target too. Extensions.csproj defaults to net48; set the ExtensionsTargetFrameworks MSBuild property (e.g. in Directory.Build.props or 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 --build

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

ProxyPortFRONTENDCONTENT_ADMINCatch-all goes to
spire-proxy30000spirespireThe Spire dev server (SPIRE_WEB_HOST:SPIRE_WEB_PORT).
classic-proxy30100storefront-apiadmin-apiThe 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, /systemresourcesadmin-api
  • /api, /identity, /account/isauthenticated, /email, /Excel, /userfiles, /sitemap, *.txtstorefront-api
  • /integrationintegration-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 start

npm run start starts two listeners. This is intentional and allows a single spire instance to be used for net48 along with net8+:

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

src/FrontEnd/config/settings.js is created automatically (copied from settings-base.js) on first run and is gitignored, so you can change the standalone API URL without affecting the repo.

7. Access and verify

WhatURL
Storefront (Spire)http://localhost:30000
Admin Consolehttp://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

ServiceImage / buildDefault port(s)Notes
mssqlmcr.microsoft.com/mssql/server:20191433sa / Password1. Data persisted in .sql/.
elasticsearchElasticsearch 5.69201
elasticsearchnextElasticsearch 7.109200
ironpdfengineIronPDF33350PDF generation.
mailhogMailHog2525 (SMTP), 8025 (UI)Captures outgoing mail.
database-updaterbuilt from src/Database.UpdaterRuns DB migration scripts, then exits.
spire-proxy…/commerce/reverse-proxy30000nginx ingress for Spire. Main entry point.
classic-proxy…/commerce/reverse-proxy30100nginx ingress for Classic.
admin-apiwebapp base + your Extensions30070apps profile (Mode A). Admin / CMS / Identity.
storefront-apiwebapp base + your Extensions30040apps profile (Mode A).
integration-apiwebapp base + your Extensions30080apps 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.