Skip to content
StarLive demo →
OverviewChecked against main · 31 Oct 20267 min read

Deployment overview

AppointMe deploys to Azure App Service as one .NET 10 container backed by Azure SQL, Key Vault and Blob Storage. Bicep provisions it, and a GitHub Actions run on a v* tag ships it.

Runs on
Azure App Service, one Linux container
Provisioned by
Bicep templates in infra/
Shipped by
GitHub Actions, on a v* tag
Default sizing
Free F1 plan, Basic SQL

One container serves both the API and the React SPA. Everything with state lives in a managed Azure service.

Browseryour domain or *.azurewebsites.net
Cloudflare Workeroptional: sends the public host as X-Original-Host
App Service containerAPI + SPA on port 8080
Azure SQLEF Core, Dapper, Wolverine and Hangfire
Key Vaultsecrets, read through Key Vault references
Blob Storagethe Data Protection key ring
Figure 1. A request reaches one container, which talks to three managed services.

src/AppointMe.Api/Dockerfile builds the image in three stages. It builds the React bundle and copies it into wwwroot, pre-generates the Wolverine handler code, and runs the API on port 8080 as a non-root user. EF Core migrations run when the container starts, before the API serves traffic.

Users sign in through Microsoft Entra External ID. Keycloak is for local development only. App Service pulls the image from Azure Container Registry with its managed identity. The Bicep also creates Application Insights and points the OpenTelemetry exporter at it, but check that data arrives before you rely on it (see the Production checklist).

infra/main.bicep deploys these modules into one resource group. Names carry the environment name, and the globally unique ones add a 6-character hash of the resource group ID.

Module Resource Devtest setting
app-insights.bicep Application Insights Workspace-based, linked to the Log Analytics workspace
app-service-plan.bicep Linux App Service plan F1 (Free)
app-service.bicep Web App for Containers System-assigned identity, HTTPS only, TLS 1.2, Key Vault references for secrets
container-registry.bicep Container Registry Basic, admin user off, no anonymous pull
custom-domain.bicep + custom-domain-ssl-binding.bicep Hostname binding + free managed certificate Only when CUSTOM_HOSTNAME is set. Needs B1 or higher.
key-vault.bicep Key Vault RBAC mode, standard tier, 7-day soft delete, no purge protection
log-analytics.bicep Log Analytics workspace PerGB2018, 30-day retention, 0.5 GB/day ingestion cap
role-assignments.bicep Two role assignments for the Web App’s identity AcrPull on the registry, Key Vault Secrets User on the vault
sql.bicep SQL server + appointme database Basic, TLS 1.2, an AllowAllAzureServices firewall rule
storage.bicep Storage account + private data-protection-keys container Standard_LRS, no public blob access

Deploy to Azure with Bicep walks through the parameters, outputs and the app settings the Web App gets.

You set up identity, secrets and the CI login once by hand. Network hardening is left for production.

Not in the Bicep Why Covered in
Entra External ID tenant and app registration You can only create the tenant in the portal Entra External ID
Key Vault secret values Secrets never go in a template or an output Azure with Bicep
The CI deployer identity and its federated credential Created once with az Azure with Bicep
Keycloak Local development only. Authentication:Provider is EntraExternalId when deployed. Entra External ID
Azure Service Bus The SqlDurable transport runs on the SQL database, so you don’t need a broker Domain events & messaging
WAF, private endpoints, VNet integration, deployment slots A demo environment doesn’t need them Production checklist

The F1 App Service plan is free, but it unloads the app when idle and caps its daily CPU. Move to B1 when that starts to hurt.

F1 (the default) B1 and up
Idle Unloads after about 20 minutes idle. The next request cold-starts the container, including EF migrations, in 30–90 s. Stays loaded if you turn on Always On, which the Bicep doesn’t set
CPU A daily quota. Once it’s used up, the app returns HTTP 403 until midnight UTC. No daily quota
Background work Hangfire jobs and Wolverine’s durability agent run only while the app is loaded Run continuously with Always On
Custom domain No hostname bindings, so the Cloudflare Worker fronts the domain CUSTOM_HOSTNAME binds it with a free managed certificate
Deployment slots No S1 or higher

main.bicep doesn’t pass a SKU, so each module’s default applies. To change the plan, edit the sku default in infra/modules/app-service-plan.bicep. The same goes for SQL (databaseSku in sql.bicep, Basic: 5 DTU, 2 GB) and the registry (sku in container-registry.bicep).

The rest is paid at its smallest tier: SQL Basic, ACR Basic, Standard_LRS storage, Key Vault operations and Log Analytics ingestion, which is capped at 0.5 GB a day. Price your region with the Azure pricing calculator.

The repo ships one deployed environment, devtest. The environment name sets the resource names and picks the appsettings file that loads.

Environment ASPNETCORE_ENVIRONMENT Config file Runs Sign-in
Local Development appsettings.Development.json Aspire or dotnet run Keycloak
devtest Devtest appsettings.Devtest.json Azure, deployed by devtest.yml Entra External ID
Codegen Codegen appsettings.Codegen.json Build time only: Wolverine codegen and the OpenAPI document Placeholder values

Bicep maps environmentName: 'devtest' to Devtest. Any other name is passed through unchanged, so staging loads appsettings.staging.json. The CI pipeline only knows devtest. A second environment needs its own .bicepparam file, a GitHub environment, a federated credential and a deploy job.

Why does the API serve the SPA instead of a separate static host?1 min · optional

The API has no CORS policy. It signs browsers in with a cookie (appointme.auth), checks antiforgery tokens, and sends a report-only CSP with connect-src 'self'. All three expect the SPA and the API on one origin.

Locally the Vite dev server proxies /api, /signin-oidc and /admin to the API to get the same effect. In the container the API serves the built bundle from wwwroot and falls back to index.html, so there’s one image to build and one host to secure.

Each step uses outputs from the one before it. Expect about an hour the first time, longer if you’re waiting on DNS.

Step Page You end up with
1. Provision Azure with Bicep, step 1 A resource group, a placeholder Web App and the deployment outputs
2. Set up sign-in Entra External ID A tenant, an app registration and appsettings.Devtest.json
3. Wire up and ship Azure with Bicep, steps 3–7 Seeded secrets, a CI identity, GitHub secrets and a first tagged deploy
4. Understand the pipeline GitHub Actions CI/CD What each workflow checks, and when it deploys
5. Harden Production checklist A list of what to change before real customers arrive

Terraform for Azure and AWS is planned for PRO.