---
title: .NET 8.0+ local development environment
slug: configured-commerce/docs/net8-local-development-environment
docTags: 
createdAt: 2026-08-13T10:46:21.000Z
---

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](docId\:yR5txbyljjyNjPSMiVTeG) 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.

```mermaid
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.

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

:::BlockQuote
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](docId\:BGQc6W-TnDZQNuTeTkO7b) for details.

## 2. Seed a database

**Start SQL Server** (and the rest of the infrastructure):

```bash
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`).

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

2. **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.
3. **Apply version migrations.** Re-run the stack so `database-updater` brings the seeded
   database up to the current version:

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

| 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](docId\:BGQc6W-TnDZQNuTeTkO7b). Both rely on the infrastructure and proxies started in [section 3](docId\:BGQc6W-TnDZQNuTeTkO7b) (`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:

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

:::BlockQuote
**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:

```bash
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**&#xA;**&#xD;**`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`)

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

```bash
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 nginx`spire-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&#x20;**`http://localhost:3000`**.**

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

| What                          | URL                                                          |
| ----------------------------- | ------------------------------------------------------------ |
| **Storefront (Spire)**        | [http://localhost:30000](http://localhost:30000)             |
| **Admin Console**             | [http://localhost:30000/admin](http://localhost:30000/admin) |
| **MailHog (outgoing mail)**   | [http://localhost:8025](http://localhost:8025)               |
| Classic storefront (optional) | [http://localhost:30100](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-updater` **fails on first run** — the `Insite.Commerce` database doesn't exist
  yet. Seed it ([section 3](docId\:BGQc6W-TnDZQNuTeTkO7b)), 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](docId\:BGQc6W-TnDZQNuTeTkO7b)) 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.
