Tools: Deploying a Full-Stack App on Azure: What I Learned Connecting a VM to a Private MySQL Database

Tools: Deploying a Full-Stack App on Azure: What I Learned Connecting a VM to a Private MySQL Database

Introduction

What I Built

Step 1: Designing the Network

Step 2: Provisioning the VM and Installing Dependencies

Step 3: Deploying the EpicBook Application

Step 4: Setting Up the Private Database

Step 5: Final Security Hardening

What I Learned

What's Next There's a difference between reading about cloud architecture and actually building it. This week, as part of my DevOps Micro Internship (DMI Cohort 2), I deployed the EpicBook web application on Microsoft Azure — a full-stack app with a React frontend, Node.js backend, and a MySQL database. What made this assignment different from previous ones wasn't just the tools. It was the deliberate security decisions I had to make at every step. This post walks through what I built, the decisions I made, and what actually clicked for me along the way. A two-tier deployment on Azure: The goal was to get the app live and accessible via the VM's public IP, while keeping the database completely hidden from the outside world. Before provisioning anything, I set up the network foundation. I created a Virtual Network (VNet) with the address space 10.0.0.0/16, then carved it into two subnets: This separation is the core of secure cloud architecture. The public subnet is where traffic enters. The private subnet is where sensitive data lives — and it should never be directly reachable from the internet. I then attached Network Security Groups (NSGs) to enforce the rules: That last rule is important. It means even if someone somehow reached the private subnet, they couldn't connect to MySQL unless they were coming from the application VM itself. I deployed the Ubuntu VM into the public subnet, assigned a public IP, and SSHed in. Then I installed everything the app needed: I verified the installations before moving forward: A small habit that saves a lot of debugging later. I cloned the repository and installed dependencies: Then I configured Nginx as a reverse proxy. The key configuration line that matters for React apps is inside the Nginx server block: The try_files $uri /index.html; line ensures React Router works correctly. Without it, refreshing any route other than / returns a 404 — because Nginx looks for a physical file that doesn't exist instead of handing control back to React. I also used environment variables to pass the database credentials to the Node.js backend. No hardcoded passwords in the codebase. I provisioned an Azure Database for MySQL Flexible Server using Private Access (VNet Integration), placing it directly inside the private subnet. This means the database has no public endpoint. The only way to reach it is from within the VNet — specifically from the application VM. I imported the SQL dump to initialize the schema, then tested the connection from the VM: Seeing the author records return in the terminal confirmed the entire chain was working: One last step beyond the basics: I restricted SSH access in the NSG to my specific local IP address only. This means Port 22 is no longer open to the entire internet — only my machine can SSH into the VM. I also verified the internal Linux firewall (ufw) was not interfering with web traffic: 1. Network design happens before deployment, not during.

VNets and subnets are not setup steps to rush through. They are the architecture. Everything else builds on top of them. 2. Private access is not optional for databases.Putting a database in a public subnet with a public endpoint is a real risk that real companies have paid for. VNet integration and NSG rules are how you protect data properly. 3. Environment variables are a non-negotiable habit.

Hardcoding credentials — even temporarily — is a risk. Using .env files from the start costs nothing and prevents a lot. Next up: the AWS portion of this project, and then I'm moving into Agentic AI for DevOps — specifically how AI agents are changing the way infrastructure is managed and automated. If you're also on the DevOps learning path, feel free to connect: linkedin.com/in/odoworitse-afari This post is part of my DMI Cohort 2 learning journey with Pravin Mishra — CloudAdvisory. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nodejs -weight: 500;">npm nginx -weight: 500;">git mysql-client -y -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nodejs -weight: 500;">npm nginx -weight: 500;">git mysql-client -y -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nodejs -weight: 500;">npm nginx -weight: 500;">git mysql-client -y node -v -weight: 500;">npm -v -weight: 500;">git --version node -v -weight: 500;">npm -v -weight: 500;">git --version node -v -weight: 500;">npm -v -weight: 500;">git --version -weight: 500;">git clone https://github.com/pravinmishraaws/theepicbook.-weight: 500;">git cd theepicbook -weight: 500;">npm -weight: 500;">install -weight: 500;">git clone https://github.com/pravinmishraaws/theepicbook.-weight: 500;">git cd theepicbook -weight: 500;">npm -weight: 500;">install -weight: 500;">git clone https://github.com/pravinmishraaws/theepicbook.-weight: 500;">git cd theepicbook -weight: 500;">npm -weight: 500;">install server { listen 80; server_name _; root /home/azureuser/theepicbook/build; index index.html; location / { try_files $uri /index.html; } } server { listen 80; server_name _; root /home/azureuser/theepicbook/build; index index.html; location / { try_files $uri /index.html; } } server { listen 80; server_name _; root /home/azureuser/theepicbook/build; index index.html; location / { try_files $uri /index.html; } } mysql -h epicbook-db1.mysql.database.azure.com \ -u admin123 \ -p bookstore \ -e "SELECT * FROM author LIMIT 5;" mysql -h epicbook-db1.mysql.database.azure.com \ -u admin123 \ -p bookstore \ -e "SELECT * FROM author LIMIT 5;" mysql -h epicbook-db1.mysql.database.azure.com \ -u admin123 \ -p bookstore \ -e "SELECT * FROM author LIMIT 5;" VM → private subnet → MySQL → data ✓ VM → private subnet → MySQL → data ✓ VM → private subnet → MySQL → data ✓ -weight: 600;">sudo ufw -weight: 500;">status # Status: inactive -weight: 600;">sudo ufw -weight: 500;">status # Status: inactive -weight: 600;">sudo ufw -weight: 500;">status # Status: inactive - Compute layer: Ubuntu 22.04 VM (Standard B1s) running the EpicBook app with Nginx as a reverse proxy - Database layer: Azure Database for MySQL Flexible Server, placed in a private subnet with no public internet access - 10.0.1.0/24 — Public subnet for the VM - 10.0.2.0/24 — Private subnet for the MySQL database - Public subnet NSG: Allow HTTP (Port 80) and SSH (Port 22) - Private subnet NSG: Allow MySQL (Port 3306) only from the VM's subnet