# Deploy a Full-Stack Node.js Application on Azure VM with Private MySQL

Deploying a full stack application to the cloud is more than creating a virtual machine and running `npm start`.

You need to think about **network segmentation, private database connectivity, security groups, reverse proxies, process management, DNS, application configuration, and troubleshooting**.

In this tutorial, we'll deploy **EpicBook**, a Node.js/Express application backed by MySQL, on Microsoft Azure.

The final architecture will use:

*   An **Azure Virtual Network (VNet)** with separate public and private subnets
    
*   An **Ubuntu 22.04 LTS Azure VM**
    
*   **Nginx** as a reverse proxy
    
*   **Node.js/Express** running the EpicBook application
    
*   **Azure Database for MySQL Flexible Server** using private VNet access
    
*   **Network Security Groups (NSGs)** to control traffic
    
*   **PM2** to keep the application running
    
*   MySQL seed files to populate application data
    

The important lesson is that moving an application to Azure doesn't eliminate troubleshooting, it introduces new infrastructure layers that you need to understand.

**What We Are Building**  
The target architecture looks like this:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/e59d1630-87e0-4c45-8594-cb7488b2f9f3.jpg align="center")

The Virtual Network will use the following address space:

| **Resource** | **CIDR** |
| --- | --- |
| VNet | `10.0.0.0/16` |
| Public/VM subnet | `10.0.1.0/24` |
| Private/MySQL subnet | `10.0.2.0/24` |

The VM is the only public entry point.  
The database is not exposed directly to the internet.  
Azure Database for MySQL Flexible Server's VNet integration requires a **delegated subnet** dedicated to MySQL Flexible Server. Azure also uses a Private DNS zone for name resolution when private access is configured through the portal.

### **Prerequisites**

Before starting, you should have:

*   An active Azure subscription
    
*   An SSH client
    
*   Basic Linux command-line knowledge
    
*   Basic Git knowledge
    
*   Familiarity with Node.js and npm
    
*   Basic understanding of networking
    
*   The EpicBook repository
    

### **Phase 1 - Understand the EpicBook Application**

Before creating infrastructure, it is important to understand what is actually being deployed.  
EpicBook consists of:

*   Node.js
    
*   Express.js
    
*   Express Handlebars
    
*   Sequelize
    
*   MySQL
    
*   Nginx
    
*   PM2
    

### **Task 0 - Create the Azure Resource Group**

A resource group provides a logical container for the Azure resources belonging to the application.  
Sign in to the Azure Portal and search for **Resource groups**.  
Select **Create**.  
Configure:  
1\. Select your Azure subscription from the drop down  
2\. Enter your preferred name for the **Resource Group**  
3\. Select a preferred **Region** to house the project

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/809060c0-3127-4fba-8e82-d101c6d71d4b.png align="center")

Select **Review + create**, allow for the configuration to be validated , then select **Create**.

You should now have:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/4885086f-2496-4192-bac8-a75989fa2622.png align="center")

This resource group will contain the major resources used throughout the deployment.

### **Task 1 - Build the Azure Network**

The network is one of the most important parts of this architecture.  
We will create:

*   One Virtual Network
    
*   One public subnet for the VM
    
*   One private subnet for MySQL
    
*   One NSG for the public subnet
    
*   One NSG for the private subnet
    
*   One public IP address for the VM
    

**Step 1.1: - Create the Virtual Network**

In the Azure Portal, search for **Virtual networks**.  
Select **Create**.  
On the Basics tab enter the following details  
Your Azure subscription, Resource Group Name, Virtual Network Name, Select the region you created the resource group in;

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/11459576-4281-4979-8d56-d7c648b42290.png align="center")

next we will add `/16` address space provides a large private address range from which Azure subnets can be allocated.

**Step 1.2: Create the Public Subnet**

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/0889642b-b45d-496f-a350-8e543e5b31b1.png align="center")

on the **Address Space** tab click on **Add a subnet** and enter the following details for the public subnet; subnet name: **epicbook-public-subnet** and address range : **10.0.1.0/24**.  
This subnet will contain the VM.  
The resulting structure is:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/169a4f2b-ff8c-4aae-bff6-85e42f6ca63e.jpg align="center")

**Step 1.3: Create the Private Subnet**  
Create another subnet with the following details subnet name: **Epicbook-private-subnet** and address range: `10.0.2.0/24`

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/d303b81e-8bcd-45fc-9f18-4fdcc366d5eb.png align="center")

This subnet is intended for Azure Database for MySQL Flexible Server.  
The Virtual Network now looks like:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/c5b54f19-4674-45b8-b0cf-4add12fdc089.jpg align="center")

**Step 1.4: Create the Public Network Security Group**  
Search for **Network security groups** in Azure Portal.  
Select **Create**.

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/28780503-f649-4e28-8c3b-ad675a7821ae.png align="center")

in the page that shows enter the following details; Your subscription\[*e.g. Azure Subscription 1*\] , Resource Group\[*e.g. Epicbook-RG*\], Network Security Group Name\[*e.g. Epicbook-public-NSG*\], same Azure region we've used all the while.  
click **Review + create**, wait for validation then click **Create** to create the Network Security Group.

**Step 1.5: Configure Public NSG Rules**  
The VM needs to receive HTTP traffic from the Internet.  
It also needs SSH access for administration. so we need to set some incoming and outgoing rules for the NSG, to set the rules, open the public NSG page. click on the **inbound security rules** tab on the left pane and then click on +Add at the top of the screen to reveal a pop up screen where you can enter the rules.

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/59f51d48-cc49-45a1-a379-fbb4c82a8668.png align="center")

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/44e72ea0-f21c-43e8-a62f-f497ca2bc53b.png align="center")

**HTTP** rules will be set as follows: Source: *any*, Source port: \*, Destination: *any*, Service: *Http*, Destination port: *80*, Protocol : *TCP*, Action: *Allow*, Priority: *110*, Name: *Allow-http*

**HTTPS** rules will be set as follows: Source: *any*, Source port: \*, Destination: *any*, Service: *Https*, Destination port: *443*, Protocol : *TCP*, Action: *Allow*, Priority: *120*, Name: *Allow-https*

For **SSH**, use a much narrower source whenever possible.  
For example: Source: *My IP address*, Service: SSH, Destination port: *22*, Protocol: *TCP*, Action: *Allow*, Priority: *100*, Name: *Allow-ssh*

**Security recommendation**  
Do **not** unnecessarily expose SSH to the entire Internet.  
Instead of:

```plaintext
0.0.0.0/0 → TCP/22
```

prefer:

```plaintext
Your trusted IP → TCP/22
```

If your public IP changes frequently, you may need to update the NSG rule.

**Step 1.6: Associate the Public NSG with the Subnet**  
Open:  
**Epicbook-Public-NSG → Subnets**

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/fe3e465a-0640-4edd-8c56-f871aa25ca54.png align="center")

Select **Associate**.  
Choose:

*   Virtual network: `Epicbook-Vnet`
    
*   Subnet: `Epicbook-public-subnet`
    

The public subnet now has the NSG applied to it.

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/8d1beb0b-6832-4f3e-9cc4-6c72c13fa6fb.png align="center")

**Step 1.7: Create the Private Network Security Group**  
Create another NSG with the following details; Resource Group: Epicbook-RG, Name: Epicbook-private-NSG, Region: Same as the public NSG.

**Step 1.8: Secure the Private MySQL Subnet**  
This subnet incoming rules will be set as follows: Source: *IP Addresses*, Source port: \*, Destination: *any*, Service: *MySql*, Destination port: *3306*, Protocol : *TCP*, Action: *Allow*, Priority: *100*, Name: *Allow-mysql.*

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/5ff505ae-69de-4d0f-b5fa-48ccce10a695.png align="center")

![]( align="center")

Then associate `Epicbook-Private-NSG` with Epicbook-private-subnet

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/02f81e91-90f2-46e2-a0cc-9b0257dd6b02.png align="center")

**Why this matters**

Your architecture should follow the principle of least privilege:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/4b781c8c-bc95-4aa0-b8eb-24bbe976a85e.jpg align="center")

The Internet should not have a direct path to MySQL.  
  
**Step 1.9: Create the Virtual Machine's Public IP**

Search for **Public IP addresses**, click on **Public IP addresses** from your search results**.** Select **Create** from the **Public IP addresses** page.  
Enter the following details under the Basics tab; Subscription, Resource Group, IP Version: leave it as IPv4, SKU: Standard, Availability Zone: Zone Redundant, Tier: Regional, Routing Preference: Microsoft Network, leave all others details as default.

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/9211d4b6-9e7b-4f8f-8e3c-d21c0de9837f.png align="center")

  
This public IP will eventually provide the application's Internet facing endpoint.  

### Task 2: Create the Azure Virtual Machine

**Step 2.1:**  
to create the Virtual Machine, on the search bar at the top of the Azure search for Virtual machines.

Select: Create → Azure virtual machine  
Configure the VM as follows:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/14dda375-d93e-4bfa-97ef-fb6138067893.png align="center")

| **Setting** | **Value** |
| --- | --- |
| Resource group | `Epicbook-RG` |
| VM name | `Epicbook-vm` |
| Region | Same region |
| Image | Ubuntu Server 22.04 LTS |
| Size | `Standard_B1s` or assignment-equivalent |

The B-series is suitable for a small development/demo workload, although it may not be appropriate for a production application with sustained CPU usage.

**Step 2.2: Configure SSH Authentication**

Under authentication:

*   Select SSH public key
    
*   Create a new SSH key or select an existing one
    
*   Specify your administrator username
    
*   Download/save the private key securely
    

For example:

```plaintext
epicbook-key.pem
```

Protect the private key:

```plaintext
chmod 600 epicbook-key.pem
```

Never commit the private key to Git.

### Step 2.3: Configure Virtual Machine Networking Configure:  

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/9da7f067-37be-40f6-a324-bdc31733ca85.png align="center")

| **Setting** | **Value** |
| --- | --- |
| Virtual network | `Epicbook-Vnet` |
| Subnet | `Epicbook-public-subnet` |
| Public IP | `Epicbook-VM-Public-IP` |
| NIC | Automatically created |

The resulting network path is:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/22aa55c9-47b5-404e-be9f-66e36002217b.jpg align="center")

Make absolutely sure the VM is not placed in the MySQL private subnet.

**Step 2.4: Deploy the Virtual Machine**  
Select: Review + create  
After validation succeeds,  
select: Create  
Wait for the deployment to complete.

**Step 2.5: Connect to Ubuntu**

Azure will provide an SSH command.  
A typical command looks like:

```plaintext
ssh -i your-key.pem username@<PUBLIC-IP>
```

For example:

```plaintext
ssh -i epicbook-key.pem azureuser@20.x.x.x
```

Once connected, verify the operating system:

```plaintext
lsb_release -a
```

You should see Ubuntu 22.04 LTS.

### **Step 2.6: Update Ubuntu**

Update the operating system packages:

```plaintext
sudo apt update 
sudo apt upgrade -y
```

Check for remaining upgrades:

```plaintext
apt list --upgradable
```

Keeping the operating system updated reduces exposure to known vulnerabilities.

**Step 2.7: Install Node.js, Nginx, Git, and MySQL Client**

Install Node.js 22:

```plaintext
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install nodejs -y 
```

Install Nginx:

```plaintext
sudo apt install nginx -y
```

Install Git:

```plaintext
sudo apt install git -y
```

Install the MySQL client:

```plaintext
sudo apt install mysql-client -y
```

Verify everything:

```plaintext
node -v
npm -v
nginx -v
git --version
mysql --version
```

**Step 2.8: Enable Nginx**  
Run:

```plaintext
sudo systemctl enable nginx 
sudo systemctl start nginx 
sudo systemctl status nginx --no-pager
```

At this point, Nginx should be running.

You can initially test it by opening the VM's IP address:

```plaintext
http://<VM-PUBLIC-IP>
```

You should see the default Nginx page.

### Task 3: Download and Inspect EpicBook

Clone the application:

```plaintext
git clone https://github.com/pravinmishraaws/theepicbook.git 
cd theepicbook
```

Install its dependencies:

```plaintext
npm install
```

You can inspect the installed packages with:

```plaintext
ls node_modules | wc -l
```

**Step 3.1: Inspect the Application Configuration**

Before changing anything, inspect the application:

```plaintext
cat package.json
cat config/config.json
cat server.js
ls db
cat "Installation & Configuration Guide.md"
```

This inspection is important because it prevents us from making assumptions about how the application works.  
  
**Understanding package.json**  
The package manifest identifies the application's dependencies and startup configuration.  
EpicBook uses technologies including:  
\- Express  
\- Express Handlebars  
\- Sequelize  
\- MySQL2  
This confirms that the application is a unified Express application rather than a separate React frontend and Node.js API.

The application can be started through:

```plaintext
node server.js
```

or its corresponding npm start script, depending on the repository's **package.json**.

**Understanding config/config.json**  
The Sequelize configuration contains separate environments:

```plaintext
development
test
production
```

Because the deployment does not set NODE\_ENV, the application will use the development configuration by default.  
The original configuration points to a local MySQL database:

```plaintext
127.0.0.1
```

That will not work because our database is hosted in Azure.  
We therefore need to replace the development database configuration with the Azure MySQL server details.  
  
**Understanding server.js**  
The server listens on:

```plaintext
8080
```

because the application uses:

```plaintext
process.env.PORT || 8080
```

The application also serves static files itself.  
Therefore Nginx does not need to serve a React build directory.  
Instead:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/05858d6d-6d35-4bbf-81f1-c86a19ad784a.jpg align="center")

The application also calls Sequelize synchronization during startup.  
This is important because the application will only begin accepting requests after the database initialization succeeds.  
  
**Understanding the db Directory**  
Inspect:

```plaintext
ls db
```

You should find files such as:

```plaintext
BuyTheBook_Schema.sql
author_seed.sql
books_seed.sql
author.csv
books.csv
```

There is an important distinction between schema creation and data seeding.  
sequelize.sync() creates database tables based on the application's Sequelize models. It does not necessarily populate those tables with the bookstore's seed data. The seed SQL files are responsible for inserting the application data.

### Task 4: Create Azure Database for MySQL Flexible Server

Now that the application requirements are understood, create the managed database.  
In Azure Portal, search for:  
**Azure Database for MySQL flexible servers**

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/49e7e420-3533-4088-b808-e995bddbaeac.png align="center")

Select Create.  
Choose the appropriate configuration experience offered by the portal.

**Step 4.1: Configure MySQL Basics**

Use:

| **Setting** | **Value** |
| --- | --- |
| Resource group | `Epicbook-RG` |
| Server name | Your globally unique server name |
| Region | Same region as VM |
| MySQL version | Current supported version |
| Workload | Development/Test |

For authentication, select:

```plaintext
MySQL authentication only
```

Choose an administrator username.  
Do not attempt to use root as the Azure administrator account.  
Choose a strong password and store it securely.

**Step 4.2: Configure Private Networking**

Select:

```plaintext
Private access / VNet Integration
```

Select:

```plaintext
Virtual network: Epicbook-Vnet
```

Select the private subnet:

```plaintext
Epicbook-private-subnet 10.0.2.0/24
```

This is what keeps the database off the public Internet.  
Your architecture should now resemble:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/feec65f1-f4d8-47d5-8ed9-a5586d2fdaa0.jpg align="center")

Review the configuration and create the server. The deployment can take several minutes.

**Step 4.3: Retrieve the MySQL Hostname After deployment completes**,  
open the MySQL Flexible Server resource. From its overview/connection information, obtain the server hostname.  
For example:

```plaintext
epicbook-server.mysql.database.azure.com
```

Use your actual hostname rather than copying the example.

### **Task 5: Connect to MySQL from the VM**

Return to the SSH session on the Ubuntu VM.  
Use the MySQL client:

```plaintext
mysql -h epicbook-server.mysql.database.azure.com -u <ADMIN_USERNAME> -p
```

If the **bookstore** database already exists, you can connect directly:

```plaintext
mysql -h epicbook-server.mysql.database.azure.com -u <ADMIN_USERNAME> -p bookstore
```

Once connected:

```plaintext
SHOW DATABASES;
```

Then:

```plaintext
SHOW TABLES;
```

If the database has just been created and does not exist yet, create it:

```plaintext
CREATE DATABASE bookstore;
```

then:

```plaintext
USE bookstore;
```

### **Task 6: Configure EpicBook to Use Azure MySQL**

Navigate back to the application:

```plaintext
cd ~/theepicbook
```

Open the Sequelize configuration:

```plaintext
nano config/config.json
```

Update the development section with your actual Azure MySQL credentials.  
A representative configuration is:

```plaintext
{
  "development": {
    "username": "<MYSQL_USERNAME>",
    "password": "<MYSQL_PASSWORD>",
    "database": "bookstore",
    "host": "<MYSQL_SERVER_HOSTNAME>",
    "dialect": "mysql",
    "dialectOptions": {
      "ssl": {
        "require": true,
        "rejectUnauthorized": false
      }
    }
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "use_env_variable": "JAWSDB_URL",
    "dialect": "mysql"
  }
}
```

**Important security warning**  
Do not publish a real password in a tutorial, Git repository, screenshot, or blog post.  
The example:

```plaintext
EpicBook2026
```

should be treated as a placeholder, not a password to reuse.

Also note that storing credentials directly in **config.json** is acceptable for demonstrating this deployment, but it is not the preferred production secret management strategy. A production deployment should use a secret management solution or environment based configuration.

**Why is SSL configured?**  
Azure Database for MySQL requires encrypted connections in many configurations.  
The Sequelize configuration therefore includes:

```plaintext
"dialectOptions": {
  "ssl": {
    "require": true,
    "rejectUnauthorized": false
  }
}
```

For production, certificate validation should be configured properly rather than simply disabling certificate verification.

**Step 6.1: Validate the JSON**  
Before starting the application, validate the configuration file:

```plaintext
cat config/config.json | python3 -m json.tool
```

If the JSON is valid, the command will format and print it without reporting a syntax error.

This simple validation step can save considerable troubleshooting time.

**Step 6.2: Start EpicBook Run:**

```plaintext
node server.js
```

A successful startup should eventually show something similar to:

```plaintext
App listening on PORT 8080
```

This indicates that the application successfully completed its startup process, including the database synchronization sequence.

**Step 6.3: Verify Database Tables Open a second SSH session to the VM.**

Connect:

```plaintext
mysql -h <MYSQL_SERVER_HOSTNAME>  -u <MYSQL_USERNAME> -p bookstore
```

Run:

```plaintext
SHOW TABLES;
```

You should now see the tables created by Sequelize. At this stage, the tables may exist but contain no bookstore records. That is expected.

### **Task 7: Import the EpicBook Seed Data**  

The application needs actual authors and books. Navigate to the database directory:

```plaintext
cd ~/theepicbook/db
```

Import the author data:

```plaintext
mysql -h <MYSQL_SERVER_HOSTNAME> -u <MYSQL_USERNAME> -p bookstore < author_seed.sql
```

Import the book data:

```plaintext
mysql -h <MYSQL_SERVER_HOSTNAME> -u <MYSQL_USERNAME> -p bookstore < books_seed.sql
```

The < operator redirects each SQL file into the MySQL client.

**Step 7.1: Verify the Data Connect again:**

```plaintext
mysql -h <MYSQL_SERVER_HOSTNAME> -u <MYSQL_USERNAME> -p bookstore
```

Check the author count:

```plaintext
SELECT COUNT(*) FROM author;
```

Expected result from the supplied seed data:

```plaintext
53
```

Check the book count:

```plaintext
SELECT COUNT(*) FROM book;
```

Expected:

```plaintext
54
```

Inspect sample records:

```plaintext
SELECT * FROM author LIMIT 5;
```

and:

```plaintext
SELECT * FROM book LIMIT 5;
```

The important point is that:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/37becc7b-3cbe-44c0-a4ae-793d58640e73.jpg align="center")

These are two separate operations.

### **Task 8: Test the Application Directly**

Before putting Nginx in front of the application, test Express itself.

From the VM:

```plaintext
curl http://localhost:8080
```

If the application is running correctly, you should receive an HTTP response.  
You can also check whether port 8080 is listening:

```plaintext
ss -lntp | grep 8080
```

You should see the Node.js process listening on port 8080. This is an important troubleshooting technique.

If:

```plaintext
curl localhost:8080
```

fails, the problem is with the Node.js application. If it works locally but:

```plaintext
http://<PUBLIC-IP>
```

fails, investigate Nginx, the NSG, or networking.  

### **Task 9: Run EpicBook with PM2 Running:**

```plaintext
node server.js
```

directly from SSH is not suitable for persistent operation. If the SSH session ends, the process can terminate.  
PM2 provides process management for Node.js applications.

Install it:

```plaintext
sudo npm install -g pm2
```

Navigate to the application:

```plaintext
cd ~/theepicbook
```

Start EpicBook:

```plaintext
pm2 start server.js --name epicbook-app
```

Check the process:

```plaintext
pm2 status
```

You should see:

```plaintext
epicbook-app online
```

View logs:

```plaintext
pm2 logs epicbook-app
```

**Step 9.1: Enable Startup Persistence**  
Save the current PM2 process list:

```plaintext
pm2 save
```

Generate the systemd startup configuration:

```plaintext
pm2 startup
```

PM2 will output a command that should be executed with sudo.  
Run the command it provides. Then save the process list again:

```plaintext
pm2 save
```

Now the application can be restored after a VM reboot. The desired lifecycle is:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/4c3355d6-20d8-4319-82a4-7461fdb2f798.jpg align="center")

**Task 10: Configure Nginx as a Reverse Proxy**  
The final component is Nginx.  
The browser should not communicate directly with port 8080.  
Instead:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/42ec3a6a-f23c-4b88-b73c-add189f4dabe.jpg align="center")

**Create an Nginx configuration:**

```plaintext
sudo nano /etc/nginx/sites-available/epicbook
```

Add:

```plaintext
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://localhost:8080;
        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;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

Save the file.

**Step 10.1: Enable the Nginx Site Create the symbolic link:**

```plaintext
sudo ln -s /etc/nginx/sites-available/epicbook /etc/nginx/sites-enabled/epicbook
```

Remove the default site:

```plaintext
sudo rm -f /etc/nginx/sites-enabled/default
```

**Test the Nginx configuration:**

```plaintext
sudo nginx -t
```

You should see successful syntax validation.

Restart Nginx:

```plaintext
sudo systemctl restart nginx
```

Confirm it is running:

```plaintext
sudo systemctl status nginx --no-pager
```

**Step 10.2: Access EpicBook Open a browser and navigate to:**

http://  
For example: http://20.x.x.x

Do not hard code an example IP from the walkthrough into your deployment. Use the actual public IP assigned to your VM.

The request flow should now be:

![](https://cdn.hashnode.com/uploads/covers/66c748bc0106cf4e80ad555c/8cc75c1e-5a15-42d3-ba86-dd7b6d9320fb.jpg align="center")

At this point, EpicBook should be accessible through the VM's public IP.

### **Summary**

In this tutorial, we deployed EpicBook on Azure using a straightforward cloud architecture built around an Ubuntu virtual machine and a managed MySQL database.

The infrastructure was divided into two network segments:

```text
VNet: 10.0.0.0/16

Public subnet:
10.0.1.0/24
    |
    +-- Ubuntu VM
    +-- Nginx
    +-- Node.js

Private subnet:
10.0.2.0/24
    |
    +-- Azure Database for MySQL
```

The VM provides the application's public entry point, while the database remains privately accessible through the Azure Virtual Network.  
We also established the difference between the application's schema and its data:

```text
Sequelize sync()
       |
       v
Database tables

Seed SQL
       |
       v
Authors + Books
```

Finally, PM2 keeps the Node.js application running, while Nginx acts as the public-facing reverse proxy:

```text
Client
  |
  | HTTP
  v
Nginx :80
  |
  | proxy
  v
Node.js :8080
  |
  v
Sequelize
  |
  | private connection
  v
Azure MySQL
```

### **Conclusion**

Deploying EpicBook demonstrates several fundamental cloud and DevOps principles in a practical Azure environment.

The most important lesson is that application deployment is not simply about getting a Node.js process to run. A reliable deployment requires **network segmentation, controlled ingress, private database connectivity, application configuration, process management, and a properly configured reverse proxy**.

The final solution keeps the public surface relatively small: Internet users interact with Nginx on the VM, while the application communicates with MySQL over the private Azure network. PM2 ensures that the Node.js process remains available after SSH disconnects and VM restarts.

There are also clear paths for future improvements. HTTPS, centralized secret management, stronger database TLS validation, restricted administrative access, monitoring, automated deployments, infrastructure as code with Terraform, and a highly available compute architecture would all move this demonstration closer to a production-grade Azure platform.

For learning Azure, Linux administration, Node.js deployment, networking, Nginx, MySQL, and DevOps fundamentals, however, this architecture provides a solid end to end example from Azure networking and compute all the way to a publicly accessible application backed by a private managed database.  
  
**P.S. This post is part of the DevOps Micro Internship (DMI) with Agentic AI — Cohort 3 — by** [**Pravin Mishra**](https://www.linkedin.com/in/pravin-mishra-aws-trainer/)**. My graded progress is public:** [**https://dmi.pravinmishra.com/s/wisegeorge1.html**](https://dmi.pravinmishra.com/s/wisegeorge1.html) **· Start your DevOps journey:** [**https://dmi.pravinmishra.com/?utm\\\_source=student&utm\\\_medium=ps-blog&utm\\\_campaign=cohort3**](https://dmi.pravinmishra.com/?utm\_source=student&utm\_medium=ps-blog&utm\_campaign=cohort3)
