A complete step-by-step guide to deploying a production-grade Next.js + Node.js + MySQL stack on Azure
Architecture Overview
→ Internet → Azure Application Gateway (Web Tier)
→ Web VM (Next.js + Nginx) — Public Subnets
→ Internal Load Balancer (App Tier)
→ App VM (Node.js backend) — Private Subnets
→ Azure Database for MySQL (HA + Read Replica) — Private Subnets
Phase 1: The Foundation
Networking — Virtual Network, 6 Subnets, NAT Gateway, Route Tables
Step 1: Create a Resource Group
All Azure resources must live inside a Resource Group. This is your project container.
- Go to the Azure Portal (portal.azure.com) > search Resource Groups > Create.
- Subscription: Select your subscription.
- Resource group name: capstone-rg
- Region: East US (or your preferred region — choose ONE and use it consistently).
- Click Review + Create > Create.
Step 2: Create the Virtual Network (VNet)
This is the Azure equivalent of a VPC with a 10.0.0.0/16 address space.
- Search Virtual Networks > Create.
- Resource group: capstone-rg
- Name: capstone-vnet
- Region: East US
- Click Next: IP Addresses.
- Set IPv4 address space: 10.0.0.0/16
- Delete any default subnet that appears. We will add ours manually.
- Click Review + Create > Create.
Step 3: Create the 6 Subnets
Navigate to capstone-vnet > Subnets. Click + Subnet for each one below:
⚠️ Critical: The db-private-a and db-private-b subnets must be delegated to Microsoft.DBforMySQL/flexibleServers. When creating these subnets, under Subnet Delegation select Microsoft.DBforMySQL/flexibleServers. Do NOT attach an NSG to delegated subnets — Azure will block it.
Step 4: Create the NAT Gateway
This allows your private App and DB servers to download packages without direct internet exposure.
- Search NAT Gateway > Create.
- Resource group: capstone-rg | Name: capstone-nat | Region: East US.
- Under Outbound IP, click Create a new public IP address. Name: capstone-nat-ip.
- Click Next: Subnet. Associate the NAT gateway with: web-public-a and web-public-b.
💡 Note: Azure NAT Gateways associate at the subnet level, not a standalone ‘attachment’. By associating it with the public subnets, resources in private subnets that route through them will use this NAT.
- Click Review + Create > Create.
Step 5: Configure Route Tables (UDRs)
Azure automatically handles basic routing, but we need custom User-Defined Routes for private subnets to reach the NAT.
Public Route Table
- Search Route Tables > Create.
- Name: capstone-public-rt | Resource group: capstone-rg | Region: East US.
- After creation, go to the resource > Settings > Subnets > Associate. Add web-public-a and web-public-b.
💡 Note: Azure VNets have built-in internet routing for public subnets. No explicit 0.0.0.0/0 → Internet route is needed unless you have overriding rules.
Private Route Table
- Create another Route Table: capstone-private-rt.
- Go to Settings > Routes > Add. Add a route: Name: default-to-nat | Address prefix: 0.0.0.0/0 | Next hop type: Internet. (Azure NAT Gateway intercepts this and applies SNAT automatically).
- Go to Settings > Subnets > Associate. Add: app-private-a, app-private-b.
💡 Note: Do NOT associate the db subnets with this route table — they are delegated to MySQL and Azure manages their routing.
Phase 2: The Firewalls
Network Security Groups (NSGs) — Azure’s equivalent to AWS Security Groups
In Azure, NSGs can be attached to subnets or individual VM NICs. We follow the same strict chain as the AWS architecture:
• Internet → App Gateway NSG
• App Gateway NSG → Web VM NSG
• Web VM NSG → Internal LB NSG
• Internal LB NSG → App VM NSG
• App VM NSG → DB NSG
⚠️ Critical: Azure NSGs use Priority numbers (100–4096). Lower number = higher priority. Always assign priorities with gaps (e.g. 100, 200, 300) to leave room for future rules.
Step 1: Public App Gateway NSG
- Search Network Security Groups > Create.
- Name: capstone-appgw-nsg | Resource group: capstone-rg.
- After creation, go to Inbound security rules > Add: • Name: Allow-HTTP-Internet | Priority: 100 | Source: Any | Dest. Port: 80 | Protocol: TCP | Action: Allow • Name: Allow-HTTPS-Internet | Priority: 110 | Source: Any | Dest. Port: 443 | Protocol: TCP | Action: Allow • Name: Allow-AppGW-Infra | Priority: 120 | Source: GatewayManager | Dest. Port: 65200–65535 | Protocol: TCP | Action: Allow
⚠️ Critical: The GatewayManager rule on ports 65200–65535 is MANDATORY for Azure Application Gateway. Without it, the App Gateway health probes will fail and it will not provision correctly.
- Attach this NSG to web-public-a and web-public-b subnets.
Step 2: Web VM NSG
- Create NSG: capstone-web-vm-nsg
- Inbound rules: • Name: Allow-HTTP-FromAppGW | Priority: 100 | Source: (App Gateway subnet CIDR 10.0.1.0/24 & 10.0.2.0/24) | Dest. Port: 80 | Action: Allow • Name: Allow-SSH-MyIP | Priority: 200 | Source: Your IP address | Dest. Port: 22 | Protocol: TCP | Action: Allow
- Attach to web-public-a and web-public-b subnets.
Step 3: Internal Load Balancer NSG
- Create NSG: capstone-internal-lb-nsg
- Inbound rules: • Name: Allow-HTTP-FromWebVMs | Priority: 100 | Source: 10.0.1.0/24, 10.0.2.0/24 | Dest. Port: 80 | Action: Allow • Name: Allow-AzureLoadBalancer | Priority: 200 | Source: AzureLoadBalancer (service tag) | Dest. Port: * | Action: Allow
- Attach to app-private-a and app-private-b subnets.
Step 4: App VM NSG
- Create NSG: capstone-app-vm-nsg
- Inbound rules: • Name: Allow-3001-FromInternalLB | Priority: 100 | Source: 10.0.3.0/24, 10.0.4.0/24 | Dest. Port: 3001 | Action: Allow • Name: Allow-SSH-FromWebVM | Priority: 200 | Source: 10.0.1.0/24, 10.0.2.0/24 | Dest. Port: 22 | Protocol: TCP | Action: Allow • Name: Allow-AzureLoadBalancer | Priority: 300 | Source: AzureLoadBalancer | Dest. Port: * | Action: Allow
- Attach to app-private-a and app-private-b subnets.
Step 5: Database NSG
- Create NSG: capstone-db-nsg
- Inbound rules: • Name: Allow-MySQL-FromAppVM | Priority: 100 | Source: 10.0.3.0/24, 10.0.4.0/24 | Dest. Port: 3306 | Protocol: TCP | Action: Allow
💡 Note: For delegated MySQL subnets, Azure may manage some NSG rules automatically. If you encounter issues, verify the NSG is not conflicting with delegation requirements.
- Attach to db-private-a and db-private-b subnets.
Phase 3: The Data Layer
Azure Database for MySQL — Flexible Server (HA + Read Replica)
Azure Database for MySQL Flexible Server is the direct replacement for AWS RDS MySQL. It supports zone-redundant high availability (equivalent to Multi-AZ) and read replicas.
Step 1: Create the Primary MySQL Flexible Server (HA)
- Search Azure Database for MySQL flexible servers > Create.
- Resource group: capstone-rg
- Server name: capstone-db (This becomes part of your hostname: capstone-db.mysql.database.azure.com)
- Region: East US
- MySQL version: 8.0
- Compute + storage: Click Configure server. Select Burstable tier > B2ms (2 vCores, 8GB RAM). This is equivalent to db.t3.micro cost-tier.
- Under High Availability: Check Enable High Availability. Mode: Zone-redundant (equivalent to RDS Multi-AZ — deploys a standby in a separate Availability Zone).
- Standby availability zone: 2 (Primary will be in Zone 1).
- Admin username: admin
- Password: Password123! (or note whatever you use)
- Click Next: Networking. Networking Configuration
- Connectivity method: Private access (VNet Integration).
- Virtual network: capstone-vnet.
- Subnet: Select db-private-a (must be the delegated subnet).
- Private DNS zone: Create new. Name: capstone-db.private.mysql.database.azure.com.
⚠️ Critical: Private DNS zone is critical. Without it, your App VMs cannot resolve the MySQL hostname. Azure creates this automatically if you select ‘Create new’.
- Click Review + Create > Create. This takes 5–10 minutes.
Step 2: Create the Initial Database
Unlike AWS RDS which lets you set an initial DB name during creation, in Azure you must create the database after the server is provisioned.
- Once the server is Available, go to the resource > Databases (left menu) > Add.
- Database name: bookreview
- Click Save.
Step 3: Create the Read Replica
- Go to your capstone-db server > Settings > Replication > Add replica.
- Server name: capstone-db-replica
- Location: East US (same region for low latency).
- Click OK. Wait 5–10 minutes for it to become available.
💡 Note: Azure MySQL Flexible Server read replicas are asynchronous, same as AWS RDS read replicas. They are read-only and can be used for reporting/analytics queries.
Phase 4: The Business Layer
Internal Load Balancer + Node.js Backend VM
Step 1: Create the App VM (Backend)
- Search Virtual Machines > Create > Azure virtual machine.
- Resource group: capstone-rg
- Virtual machine name: capstone-app-vm
- Region: East US | Availability zone: Zone 1
- Image: Ubuntu Server 22.04 LTS
- Size: Click See all sizes. Choose Standard_B1ms (1 vCPU, 2GB) — equivalent to t2.micro.
- Authentication type: SSH public key.
- Username: azureuser
- SSH public key source: Generate new key pair. Name: capstone-key. Download the .pem file when prompted.
- Click Next: Disks > Next: Networking. Networking Settings
- Virtual network: capstone-vnet
- Subnet: app-private-a
- Public IP: None (CRITICAL — must be private only!)
- NIC network security group: None (our NSG is attached to the subnet already).
- Click Review + Create > Create.
Step 2: Create the Web VM (Jump Host + Frontend)
- Create another VM: capstone-web-vm
- Region: East US | Availability zone: Zone 1
- Image: Ubuntu Server 22.04 LTS | Size: Standard_B1ms
- SSH key: Use existing key (upload your capstone-key.pub).
- Networking: Virtual network: capstone-vnet | Subnet: web-public-a
- Public IP: Create new. Name: capstone-web-pip | SKU: Standard | Assignment: Static.
- NIC network security group: None.
- Click Review + Create > Create. ** Step 3: Connect and Configure the App VM (SSH Jump)** Because the App VM has no public IP, you must SSH into the Web VM first, then jump to the App VM. From your local machine — copy the key to the Web VM scp -i capstone-key.pem capstone-key.pem azureuser@:/home/azureuser/
NOTE: “azureuser” is my VMs username, so replace it with your VMs username
SSH into the Web VM
ssh -i capstone-key.pem azureuser@
From inside Web VM — jump to App VM
chmod 400 capstone-key.pem
ssh -i capstone-key.pem azureuser@
Step 4: Configure the Node.js Backend
Run the following commands inside the App VM:
Install Packages
sudo apt update && sudo apt install nodejs npm mysql-client -y
Clone and Configure the App
git clone https://github.com/pravinmishraaws/book-review-app.git
cd book-review-app/backend
npm install
sudo npm install -g pm2
Edit the Environment File
nano .env
Update these values:
• DB_HOST=capstone-db.mysql.database.azure.com
• DB_USER=admin
• DB_PASSWORD=Password123!
• DB_NAME=bookreview
• PORT=3001
💡 Note: The Azure MySQL hostname format is .mysql.database.azure.com — find it on the Azure Portal under your MySQL Flexible Server > Overview > Server name.
Seed and Start the Backend
node src/server.js
You should see ‘Server running on port 3001’ and ‘Database connected’. Press Ctrl+C to stop.
pm2 start src/server.js — name “backend”
pm2 save
pm2 startup
Copy and run the generated sudo command that pm2 startup outputs.
pm2 status
Step 5: Create the Internal Load Balancer
Create the Backend Pool
- Search Load Balancers > Create.
- Name: capstone-internal-lb | SKU: Standard | Type: Internal | Tier: Regional.
- Virtual network: capstone-vnet | Subnet: app-private-a.
- IP address assignment: Dynamic.
- Click Next: Backend Pools > Add a backend pool.
- Name: capstone-app-pool. Add capstone-app-vm by NIC.
- Click Next: Inbound Rules > Add a load balancing rule.
- Name: app-lb-rule | Frontend IP: (auto-assigned private IP) | Protocol: TCP | Port: 80 | Backend port: 3001.
- Health probe: Create new. Name: app-health-probe | Protocol: HTTP | Port: 3001 | Path: /
- Click Review + Create > Create.
💡 Note: Note the Internal LB’s private IP address from the Overview page. This is your equivalent to the Internal ALB DNS name.
Phase 5: The Presentation Layer
Web VM (Next.js + Nginx) + Azure Application Gateway
Step 1: Configure the Web VM (Frontend)
SSH back into your Web VM. Run these commands:
sudo apt update && sudo apt install nodejs npm nginx -y
sudo npm install -g pm2
git clone https://github.com/pravinmishraaws/book-review-app.git
cd book-review-app/frontend
npm install
Step 2: Create the Environment File
nano .env
Add this line:
NEXT_PUBLIC_API_URL=/api
Save and exit (Ctrl+O, Enter, Ctrl+X).
Step 3: Build and Start Next.js
npm run build
pm2 start npm — name “frontend” start
pm2 save && pm2 startup
Run the generated sudo command from pm2 startup.
Step 4: Configure Nginx
sudo nano /etc/nginx/sites-available/default
Delete everything and paste this configuration (replace the proxy_pass address with your Internal LB private IP):
server {
listen 80;
server_name _;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection ‘upgrade’;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}
Save and exit (Ctrl+O, Enter, Ctrl+X)
Then,
sudo nginx -t
sudo systemctl restart nginx
Step 5: Create the Azure Application Gateway (Public ALB)
Azure Application Gateway is the equivalent of AWS’s Public Application Load Balancer. It operates at Layer 7.
- Search Application Gateway > Create.
- Name: capstone-appgw | Region: East US | Tier: Standard V2.
- Autoscaling: Minimum 1, Maximum 2 (cost saving).
- Virtual network: capstone-vnet | Subnet: web-public-a.
- Click Next: Frontends.
- Frontend IP type: Public. Create new public IP: capstone-appgw-ip.
- Click Next: Backends. Add a backend pool.
- Name: capstone-web-pool. Add capstone-web-vm (by NIC or IP).
- Click Next: Configuration. Add a routing rule.
- Rule name: http-rule | Priority: 100.
- Listener: Name: http-listener | Frontend IP: Public | Protocol: HTTP | Port: 80.
- Backend targets: Target type: Backend pool | Backend pool: capstone-web-pool.
- HTTP settings: Create new. Name: web-http-settings | Protocol: HTTP | Port: 80.
- Click Review + Create > Create. This takes 5–10 minutes.
💡 Note: After creation, get the Application Gateway’s public IP from the Frontend IP configurations. This is what your users will browse to.
POSSIBLE PROBLEM YOU MIGHT ENCOUNTER IN STEP 5
I keep getting this error at step 5 “Subnet must only have application gateway”
This is a very common Azure Application Gateway gotcha.
Exact cause. The error is clear and the fix is straightforward. Here’s what’s happening and how to fix it:
The Problem
Azure Application Gateway requires a dedicated subnet — you cannot deploy any other resource in the Application Gateway subnet.
You are trying to deploy the Application Gateway into web-public-a, which already has your Web VM sitting in it. Azure is rejecting this because the subnet is not empty.
The Fix: Create a dedicated subnet for the Application Gateway
Go to capstone-vnet > Subnets > + Subnet and add a new one:
Name: appgw-subnet
CIDR: 10.0.7.0/24
NSG: capstone-appgw-nsg
Then go back to your Application Gateway creation and select appgw-subnet instead of web-public-a.
Step 6: Update CORS in the Backend
SSH back to the App VM (via Web VM jump), then update CORS:
cd ~/book-review-app/backend
nano .env
Add or update:
ALLOWED_ORIGINS=http://
pm2 restart backend
Open your browser and navigate to: http://
The Book Review App should load successfully!
QUICK TEST:
Restart one of the DBs and see if your application is still reachable
Restart the VM also to see if pm2 auto starts the application.
OPTIONAL
Phase 6: High Availability with VM Scale Sets
Equivalent to AWS Auto Scaling Groups (ASGs) with Launch Templates
Step 1: Create Golden VM Images
Before scaling, capture the perfectly configured VMs as reusable images.
- Go to capstone-app-vm > Click Capture (from the top menu).
- Share image to Azure Compute Gallery: No (use Managed Image for simplicity).
- Image name: capstone-app-golden-image.
- Check: Automatically delete this virtual machine after creating the image.
- Click Review + Create > Create. Wait for the image to be created.
- Repeat for capstone-web-vm. Image name: capstone-web-golden-image.
⚠️ Critical: Capturing an image deallocates and deletes the source VM. Make sure your backend is fully configured and tested before capturing.
Step 2: Create the App Tier VM Scale Set (VMSS)
- Search Virtual machine scale sets > Create.
- Resource group: capstone-rg | Name: capstone-app-vmss.
- Region: East US | Availability zones: 1, 2.
- Image: Click See all images > My images tab > Select capstone-app-golden-image.
- Size: Standard_B1ms.
- Authentication: SSH key > username: ubuntu > Use existing key.
- Scaling: Scaling mode: Autoscaling. Instances: Min 2, Max 4, Default 2.
- Click Next: Networking.
- Virtual network: capstone-vnet | NIC subnet: app-private-a.
- Load balancing: Select Load balancer > capstone-internal-lb > Backend pool: capstone-app-pool.
- Public inbound ports: None.
- Click Review + Create > Create.
Step 3: Create the Web Tier VM Scale Set (VMSS)
- Create another scale set: capstone-web-vmss.
- Image: capstone-web-golden-image.
- Availability zones: 1, 2.
- Scaling: Min 2, Max 4, Default 2.
- Networking: VNet: capstone-vnet | Subnet: web-public-a.
- Public IP per instance: Enabled (so each VM gets a public IP for the App Gateway health checks).
- Load balancing: Application Gateway > capstone-appgw > Backend pool: capstone-web-pool.
- Click Review + Create > Create.
Step 4: Chaos Test — Prove High Availability
Your architecture is now highly available. Prove it:
- Go to Virtual Machines. Manually stop or delete one of the VMSS instances.
- The VMSS will detect the missing instance and auto-launch a replacement using the golden image.
- pm2 will auto-start the application on the new VM.
- Wait 3–5 minutes, then refresh the Application Gateway URL. The app will still be running.
Phase 7: Cleanup — Destroy All Resources
Always delete resources after your lab to avoid unnecessary charges. Follow this order:
Step 1: Delete Scale Sets & VMs
- Go to Virtual machine scale sets > Delete capstone-app-vmss and capstone-web-vmss.
- Go to Virtual machines > Delete any remaining VMs.
Step 2: Delete Load Balancers & Application Gateway
- Search Application Gateway > Delete capstone-appgw.
- Search Load Balancers > Delete capstone-internal-lb.
- Go to Public IP addresses > Delete capstone-appgw-ip and capstone-web-pip and capstone-nat-ip.
Step 3: Delete MySQL Flexible Server
- Search Azure Database for MySQL flexible servers.
- Delete capstone-db-replica first.
- Delete capstone-db (primary). Confirm deletion — no final snapshot needed for lab cleanup.
- Delete the Private DNS Zone: capstone-db.private.mysql.database.azure.com.
Step 4: Delete NAT Gateway
- Search NAT Gateways > Delete capstone-nat.
- Delete capstone-nat-ip public IP address. Step 5: Delete the Resource Group (Nuclear Option)
- Go to Resource Groups > capstone-rg.
- Click Delete resource group.
- Type the resource group name to confirm > Delete.
⚠️ Critical: Deleting the resource group deletes ALL resources inside it in one sweep — VNet, subnets, NSGs, route tables, VMs, disks, and everything else. This is the fastest cleanup method.
Step 6: Delete VM Images & Key
- Search Images > Delete capstone-app-golden-image and capstone-web-golden-image.
- If you stored the SSH key in Azure Key Vault or SSH Keys resource, delete it.
And its a wrap
Thank you for the opportunity to share my little knowledge. I hope this was informative.
Comments are welcome.
Till next time, always stay positive 👍
Shout out to Pravin Mishra, Lead Co-Mentor: Praveen Pandey
🤝 Co-Mentors: Egwu Oko, Tanisha Borana, Ranbir Kaur
P.S. This post is part of the DevOps Micro Internship (DMI) Cohort-2 by Pravin Mishra. You can start your DevOps journey by joining this
Discord community ( https://lnkd.in/e4wTfknn ).


Top comments (0)