* chore(deps): remove Node.js community plugin * refactor(AppHost): integrate web frontend with Aspire JS hosting * docs(AppHost): update web frontend and Aspire documentation * feat(AppHost): configure Azurite emulator with persistent data volume * docs(AppHost): fix ClientsPath description to reference worktrees section * fix(Apphost): run dotnet format
Aspire AppHost
.NET Aspire orchestrates the entire Bitwarden server local development environment — supporting infrastructure and the Bitwarden application services — from a single command, replacing the manual docker-compose workflow.
Prerequisites
| Requirement | Notes |
|---|---|
| .NET SDK 10 | Required by Aspire |
| Docker Desktop | Runs the supporting infrastructure containers |
PowerShell (pwsh) |
Used by migration and secrets scripts |
| Completed server setup | dev/secrets.json must exist — follow the setup guide first |
Quick Start
cd AppHost
dotnet run
The Aspire dashboard opens automatically in your browser. All resources start in dependency order; the services wait for the database and secrets setup to finish before launching.
What Gets Started
| Resource | Type | Purpose |
|---|---|---|
setup-secrets |
Executable | Runs dev/setup_secrets.ps1 — applies dev/secrets.json to all projects |
mssql |
SQL Server 2022 container | Persistent data volume, port 1433 |
run-db-migrations |
Executable | Runs dev/migrate.ps1 against vault_dev (or self_host_dev) |
azurite |
Azure Storage emulator | Blob :10000 · Queue :10001 · Table :10002 · persistent data volume |
azurite-setup |
Executable | Runs dev/setup_azurite.ps1 after Azurite is ready |
mailcatcher |
Container | SMTP :10250 · Web UI :1080 |
redis |
Container | Redis with AOF persistence, port 6379 |
idp |
SimpleSAMLphp container | SAML IdP for SSO testing (explicit start from the dashboard) |
admin |
.NET project | Admin portal |
api |
.NET project | Main API (waits for Azurite) |
billing |
.NET project | Billing service |
events |
.NET project | Events service (waits for Azurite) |
eventsProcessor |
.NET project | Events processor (waits for Azurite) |
icons |
.NET project | Icons service |
identity |
.NET project | Identity / auth service |
notifications |
.NET project | Notifications service (waits for Azurite) |
scim |
.NET project | SCIM provisioning service |
sso |
.NET project | SSO service |
Configuration
All configuration lives in appsettings.Development.json(see Security).
Service Ports
Each service's BasePort is pre-filled in appsettings.Development.json to match the port
defined in each service's own Properties/launchSettings.json. No action is needed unless a port
conflicts with something else on your machine — in that case override it via user secrets:
dotnet user-secrets set "Services:api:BasePort" "4001"
Database Password
dotnet user-secrets set "Database:Password" "<your-sa-password>"
Full Configuration Reference
| Key | Default | Description |
|---|---|---|
SelfHost |
false |
Switch to self-hosted mode (see Self-Hosted Mode) |
ClientsPath |
../../clients/apps |
Path to the clients repo's apps/ directory (see Git Worktrees) |
WorkingDirectory |
../dev |
Directory where dev scripts are resolved |
Services:<name>:BasePort |
see appsettings.Development.json |
HTTP port for each service; pre-filled to match each service's launchSettings.json |
Database:Image |
mssql/server:2022-latest |
Docker image for SQL Server |
Database:Port |
1433 |
Host port mapped to the SQL Server container |
Database:Password |
(empty) | SA password for the SQL Server container |
Database:SelfHostPassword |
(empty) | SA password used in self-hosted mode |
Scripts:DbMigration |
migrate.ps1 |
Migration script filename (relative to WorkingDirectory) |
Scripts:AzuriteSetup |
setup_azurite.ps1 |
Azurite setup script filename |
Scripts:SecretsSetup |
setup_secrets.ps1 |
Secrets setup script filename |
MailCatcher:Image |
sj26/mailcatcher:latest |
Docker image for MailCatcher |
MailCatcher:SmtpPort |
10250 |
Host SMTP port |
MailCatcher:WebPort |
1080 |
MailCatcher web UI port |
NgrokAuthToken |
(empty) | ngrok auth token (used only when ngrok plugin is enabled) |
WebFrontend:Port |
8080 |
Web frontend port |
WebFrontend:Url |
https://bitwarden.test:8080 |
Web frontend URL shown in the dashboard |
Optional Features
Web Frontend
Runs the web client alongside the server services. Requires the Bitwarden
clients repo cloned as a sibling to server.
-
If the clients repo is not at
../../clients/apps, override the path:dotnet user-secrets set "ClientsPath" "<path/to/clients/apps>" -
Run
dotnet runas normal. Theweb-frontendresource starts with explicit start — open the Aspire dashboard and start it manually when you're ready.
Ngrok (Billing Webhook Tunneling)
Exposes the billing service through a public ngrok tunnel, useful for testing Stripe webhooks locally.
-
Create an
AppHost.csproj.userfile next toAppHost.csproj(it is covered by.gitignore):<Project> <PropertyGroup> <EnableNgrokCommunityPlugin>true</EnableNgrokCommunityPlugin> </PropertyGroup> </Project> -
Set your ngrok auth token:
dotnet user-secrets set "NgrokAuthToken" "<your-ngrok-auth-token>" -
The
billing-webhook-ngrok-endpointresource starts with explicit start — launch it from the Aspire dashboard when you need the tunnel active.
Dynamic Additional Projects
Load an extra project into the orchestration without touching source files — useful for temporary integrations or in-progress work:
# Add a project
dotnet user-secrets set "AdditionalProjects:<name>:Path" "<relative/path/to/Project.csproj>"
# Optionally wire it as a reference into an existing service
dotnet user-secrets set "AdditionalProjects:<name>:ReferencedBy:0" "api"
Replace <name> with any identifier you choose. Multiple ReferencedBy entries are indexed (0,
1, 2, …).
Self-Hosted Mode
Switch to a self-hosted database configuration:
dotnet user-secrets set "SelfHost" "true"
dotnet user-secrets set "Database:SelfHostPassword" "<password>"
In self-hosted mode:
- The database name changes from
vault_devtoself_host_dev - The migration script receives the
-self-hostedflag - Each service's effective port becomes
BasePort + 1
Aspire Dashboard
The dashboard opens automatically when you run the AppHost. You can also navigate to it directly:
| Profile | URL |
|---|---|
| HTTPS (default) | https://localhost:17271 |
| HTTP | http://localhost:15055 |
The dashboard shows live resource status, structured logs, distributed traces, and environment variables for every resource.
Security — Do Not Commit Local Settings or Tokens
Warning: Never commit local configuration values or secrets to the repository.
appsettings.Development.jsonis checked in with intentionally empty defaults. Local overrides belong in user secrets, not in that file — edits to it will appear ingit diffand risk accidental commit.Database:Password,Database:SelfHostPassword, andNgrokAuthTokenare sensitive — always store them withdotnet user-secrets, never in anyappsettings.*.jsonfile.- User secrets are stored outside the repo in your OS profile, keyed by the
UserSecretsIdinAppHost.csproj, and are never tracked by git. - If you create an
appsettings.local.json, add it to.gitignorebefore writing any values to it.
Git Worktrees
Path-based settings resolve relative to where you run dotnet run. If your worktree lives in
a different location than the main checkout, those paths won't resolve correctly. Use absolute
paths for any such setting:
-
ClientsPathdefaults to../../clients/apps— override if your worktree is not alongside theclientsrepo:dotnet user-secrets set "ClientsPath" "<absolute/path/to/clients/apps>" -
AdditionalProjects:<name>:Path— use an absolute path when adding projects via user secrets in a worktree:dotnet user-secrets set "AdditionalProjects:<name>:Path" "<absolute/path/to/Project.csproj>"
Troubleshooting
| Symptom | Fix |
|---|---|
| Secrets not applied to services | Re-run setup-secrets from the Aspire dashboard, or verify dev/secrets.json exists |
| SQL Server container won't start | Confirm Docker Desktop is running and port 1433 is free |
| Migrations fail immediately | Ensure pwsh (PowerShell) is on your $PATH |
| Port conflicts on startup | Set the conflicting Services:<name>:BasePort to a free port via user secrets |
| Services stuck waiting | Check the dashboard logs for setup-secrets or run-db-migrations errors |