This sample demonstrates an ASP.NET Core Razor Pages single-page web application called Vacation Planner hosted on an Azure Web App. The app runs on an Azure App Service Plan and stores activity data in the activities table of the PlannerDB database on an Azure Database for PostgreSQL flexible server. The server is reached through a Private Endpoint (group postgresqlServer) with the privatelink.postgres.database.azure.com Private DNS Zone, while a permissive server-level firewall rule lets the deploy machine run the post-create psql bootstrap that creates the application role and seeds the schema.
The web app enables users to plan and manage vacation activities; all data is persisted in PostgreSQL. The solution is composed of the following Azure resources:
- Azure Resource Group: A logical container scoping all resources in this sample.
- Azure Virtual Network: Hosts two subnets:
- app-subnet: Delegated to
Microsoft.Web/serverFarmsfor regional VNet integration of the Web App. - pe-subnet: Hosts the Private Endpoint to the PostgreSQL flexible server.
- app-subnet: Delegated to
- Azure Private DNS Zone
privatelink.postgres.database.azure.com, linked to the VNet. The Private Endpoint's DNS-zone group auto-registers theArecord for the server, so the Web App resolves the server's private IP through the VNet. - Azure Private Endpoint (group
postgresqlServer): Secures access to the PostgreSQL flexible server from the VNet. - Azure NAT Gateway: Deterministic outbound connectivity for both subnets.
- Azure Network Security Group: One NSG per subnet.
- Azure Log Analytics Workspace: Centralizes diagnostic logs and metrics.
- Azure Database for PostgreSQL flexible server: Public-access server hosting the
PlannerDBdatabase. BurstableStandard_B1ms, version 16, 32 GiB storage, 7-day backup retention, HA disabled. A permissive firewall rule (0.0.0.0–255.255.255.255) is created so the deploy machine can run the post-create psql bootstrap; the Web App itself reaches the server through the Private Endpoint. - PostgreSQL database
PlannerDB: Created at provisioning time; the post-deploy psql step creates theactivitiestable and seeds three demo rows. - Azure App Service Plan: The underlying compute tier that hosts the web application.
- Azure Web App: Runs the ASP.NET Core Vacation Planner app with regional VNet integration into app-subnet. The Web App connects to PostgreSQL using a dedicated application role (
testuser) — the server-admin login is never used at runtime. - App Service Source Control: (Optional) Configures continuous deployment from a public GitHub repository.
The deploy scripts follow the same pattern as the sibling web-app-sql-database sample: after provisioning, they (i) connect as the server admin via the public endpoint + firewall rule, (ii) create the application role testuser with its own password, (iii) grant minimum schema privileges on PlannerDB, (iv) create the activities table, (v) seed three sample rows, and (vi) write PG_USER=testuser + PG_PASSWORD onto the Web App's app settings. The server-admin login is never written into the Web App's runtime configuration.
- lstk CLI
- Azure Subscription
- Azure CLI
- .NET SDK 10.0
- ASP.NET Core
- Npgsql
- PostgreSQL client tools (
psql) — required by the deploy scripts to create the application role and seed data - Bicep extension, if you plan to install the sample via Bicep
- Terraform, if you plan to install the sample via Terraform
Important
Macs with Apple silicon: this sample deploys a web app from code. The emulator builds it with Microsoft's Oryx build image and runs it on an Oryx runtime image, and Microsoft publishes both for amd64 only (microsoft/Oryx#2133 asks for arm64). On a Mac with Apple silicon they run only under Docker Desktop's amd64 emulation: in Docker Desktop's settings, under General, select the Apple Virtualization framework as the Virtual Machine Manager and turn on Use Rosetta for x86_64/amd64 emulation on Apple Silicon. Under QEMU, which Docker Desktop uses otherwise (Rosetta off, or Docker VMM, which has no Rosetta), the sample is not expected to work: Oryx is a .NET app, and .NET does not support QEMU. The sample does not run on an arm64 Linux host without amd64 emulation.
Set up the Azure emulator using the LocalStack for Azure Docker image. Before starting, ensure you have a valid LOCALSTACK_AUTH_TOKEN. Refer to the Auth Token guide to obtain yours. Pull and start the emulator:
docker pull localstack/localstack-azure
export LOCALSTACK_AUTH_TOKEN=<your_auth_token>
lstk start --type azure
# Route all Azure CLI calls to the LocalStack Azure emulator
lstk az start-interceptionDeploy the application using one of these methods:
All three variants provision the same topology: VNet + pe-subnet hosting a Private Endpoint targeting a public-access PostgreSQL flexible server, with a Private DNS Zone linked to the VNet.
Note
When you deploy the application to LocalStack for Azure for the first time, the initialization process pulls and builds Docker images (LocalStack itself plus the postgres:18 backing container for the flexible-server emulator). This is a one-time operation — subsequent deployments are much faster.
- Retrieve the host port Docker maps to the port the Web App listens on in its container, for example with
docker port <container-name>(the container's name starts withls-<web-app-name>). Do not assume port 80: the app listens on the port itsWEBSITES_PORTapp setting names, when it has one. - Open a web browser and navigate to
http://localhost:<published-port>. - If the deployment was successful, you will see the Vacation Planner UI with the three seeded activities (Go to Paris, Go to London, Go to Mexico) and can add, edit, and remove activities.
You can use the scripts/call-web-app.sh Bash script to call the web app from outside the emulator. The script demonstrates four call paths:
- Through the LocalStack for Azure emulator via the default hostname.
- Via localhost and host port mapped to the container's port
80. - Via container IP address on port
80. - Via the default hostname
<web-app-name>.azurewebsites.azure.localhost.localstack.cloud:4566.
You can use pgAdmin to explore and manage the deployed database. Connect using:
| Field | Value |
|---|---|
| Host | localhost |
| Port | (see docker ps for the host-mapped port of the backing postgres:18 container) |
| Database | PlannerDB |
| Username | testuser (or pgadmin for admin operations) |
| Password | TestP@ssw0rd123 (or P@ssw0rd1234! for the admin) |
Or use psql:
PGPASSWORD='TestP@ssw0rd123' psql -h localhost -p <port> -U testuser -d PlannerDB
PlannerDB=> SELECT id, username, activity, created_at FROM activities;
