Introduction
When most beginners deploy their first cloud application,
They put everything on one server, open all the ports,
and call it done.
I wanted to do it differently.
In this project, I deployed The EpicBook — a full-stack
Node.js + MySQL web application — on Microsoft Azure using
the same network architecture patterns that real engineering
teams use in production. Public subnet for the app. Private
subnet for the database. Zero internet exposure for MySQL.
This post walks through exactly how I did it, what broke,
and what I learned.
What I Built
Here is the full architecture at a glance:
[ Internet / Users ]
│
▼
[ Azure VM: Ubuntu 22.04 ]
Nginx (reverse proxy, port 80)
Node.js/Express (port 8080)
│
▼ port 3306 (internal only)
[ Azure MySQL Flexible Server ]
Private subnet — no public IP
Tech Stack:
- Microsoft Azure (VNet, VM, MySQL Flexible Server, NSG)
- Ubuntu 22.04 LTS
- Node.js + Express.js
- MySQL 8.0
- Nginx (reverse proxy)
- Sequelize ORM
Step 1: Designing the Network
The foundation of this project is the network design.
I created one Virtual Network (VNet) with two subnets:
| Subnet | CIDR | Purpose |
|---|---|---|
| public-subnet | 10.0.1.0/24 | VM (internet-facing) |
| private-subnet | 10.0.2.0/24 | MySQL (no internet) |
Then I created two Network Security Groups (NSGs):
public-nsg (attached to the VM subnet):
- Allow SSH (port 22) from anywhere
- Allow HTTP (port 80) from anywhere
private-nsg (attached to the MySQL subnet):
- Allow MySQL (port 3306) ONLY from 10.0.1.0/24
That last rule is the most important one in the entire
project. It means the database is completely unreachable
from the internet — only the VM can connect to it.
Step 2: Provisioning the VM
I launched a Standard B1s Ubuntu 22.04 VM in the
public subnet and installed all required dependencies:
sudo apt update && sudo apt upgrade -y
sudo apt install nodejs npm nginx git mysql-client -y
Quick version check to confirm everything is installed:
node -v && npm -v && nginx -v && git --version && mysql --version
Step 3: Deploying the Application
I cloned the EpicBook repository and installed dependencies:
git clone https://github.com/pravinmishraaws/theepicbook.git
cd theepicbook
npm install
Discovering the App Structure
This is where my first learning moment happened.
The assignment described EpicBook as a "React app" — so I
tried running npm run build. It failed immediately:
npm ERR! Missing script: "build."
After inspecting the folder structure more carefully:
theepicbook/
├── server.js ← Express entry point
├── views/ ← Handlebars templates (not React!)
├── public/ ← Static assets
├── routes/ ← API and page routes
├── models/ ← Sequelize database models
└── config/
└── config.json ← Database connection config
EpicBook is a server-rendered Express app using
Handlebars templates — not a React SPA. There is no
build step. Nginx just needs to proxy all traffic to
the Node.js server on port 8080.
Lesson learned: Always read the actual code before
following generic instructions. The folder structure
tells you everything.
Configuring Nginx
sudo nano /etc/nginx/sites-available/default
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;
}
}
sudo nginx -t
sudo systemctl reload nginx
Step 4: Setting Up Azure Database for MySQL
I created an Azure Database for MySQL Flexible Server
with these critical settings:
- Connectivity method: Private access (VNet Integration)
- Virtual network: epicbook-vnet
- Subnet: private-subnet
- MySQL version: 8.0
The "Private access" setting is what places the MySQL
server inside the private subnet with no public IP.
It cannot be reached from the internet at all.
The Database Name Gotcha
Here is the second problem I ran into. The SQL seed
files were hardcoded to use a database called bookstore:
mysql -u adminuser -p epicbook < db/BuyTheBook_Schema.sql
# ERROR 1049 (42000): Unknown database 'bookstore'
The fix was simple — create the database with the
correct name:
CREATE DATABASE bookstore;
Then import all three SQL files:
mysql -h your-server.mysql.database.azure.com \
-u adminuser -p bookstore < db/BuyTheBook_Schema.sql
mysql -h your-server.mysql.database.azure.com \
-u adminuser -p bookstore < db/author_seed.sql
mysql -h your-server.mysql.database.azure.com \
-u adminuser -p bookstore < db/books_seed.sql
Verify the tables were created:
USE bookstore;
SHOW TABLES;
+---------------------+
| Tables_in_bookstore |
+---------------------+
| author |
| book |
| cart |
+---------------------+
Updating the Database Connection
I updated config/config.json to point to Azure MySQL:
{
"development": {
"username": "adminuser",
"password": "YourPassword",
"database": "bookstore",
"host": "your-server.mysql.database.azure.com",
"dialect": "mysql",
"dialectOptions": {
"ssl": {
"require": true,
"rejectUnauthorized": false
}
}
}
}
The dialectOptions.ssl block is required because Azure
MySQL enforces SSL connections by default.
Step 5: Starting the App and Testing
cd ~/theepicbook
node server.js &
Output:
App listening on PORT 8080
Opening the browser at http://VM_PUBLIC_IP — the
EpicBook application loaded with all books visible,
cart functionality working, and orders processing
correctly. Full end-to-end validation complete.
Challenges I Faced
| Challenge | What Happened | How I Fixed It |
|---|---|---|
| npm run build failed | App is Express/Handlebars, not React | Removed build step, used proxy only |
| Wrong database name | SQL files hardcoded to bookstore
|
Created correct DB name |
| Port already in use | Started server twice with &
|
Killed process with sudo kill $(lsof -t -i:8080)
|
| SSL connection error | Azure MySQL requires SSL | Added dialectOptions.ssl to config.json |
Key Takeaways
1. Private subnets are non-negotiable for databases
The MySQL server in this project has no public IP. It
exists only inside the VNet. Even if an attacker knew
the hostname, they cannot reach it from the internet.
2. NSG rules are your first line of defence
Setting the MySQL NSG source to 10.0.1.0/24 means
only the VM subnet can connect on port 3306. Everything
else is silently dropped.
3. Always inspect the actual code
Generic deployment guides don't always match the actual
application. Reading server.js and the folder structure
saved me hours of debugging.
4. Nginx as a reverse proxy is powerful
By proxying all traffic through Nginx on port 80, the
Node.js app never needs to be directly exposed. Nginx
handles SSL termination, load balancing, and caching
in real production setups.
Architecture Summary
Internet
│
▼
Azure VM (public-subnet: 10.0.1.0/24)
├── Nginx (port 80) → proxy_pass → Node.js (port 8080)
└── Node.js/Express → Sequelize ORM
│
▼ port 3306 (VNet internal only)
Azure MySQL Flexible Server (private-subnet: 10.0.2.0/24)
├── No public IP
├── VNet Integration
└── NSG: allow 3306 from 10.0.1.0/24 only
Conclusion
This project taught me that deploying an application
Correctness is about much more than making it run.
It is about understanding WHY each architectural
decision exists — and how those decisions protect
real users and real data.
The three-tier pattern (internet → app server →
private database) is not just a cloud concept. It is
the foundation of every secure production deployment
I will build in my career.
If you are working through similar Azure projects or
have questions about any of the steps above? Drop a
comment below — I would love to connect!
All commands in this post were tested on Ubuntu 22.04
LTS with Node.js v18, MySQL 8.0, and Nginx 1.24.













Top comments (0)