DEV Community

Odoworitse Afari
Odoworitse Afari

Posted on

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

Introduction

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.


What I Built

A two-tier deployment on Azure:

  • 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

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.


Step 1: Designing the Network

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:

  • 10.0.1.0/24 — Public subnet for the VM
  • 10.0.2.0/24 — Private subnet for the MySQL database

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:

  • Public subnet NSG: Allow HTTP (Port 80) and SSH (Port 22)
  • Private subnet NSG: Allow MySQL (Port 3306) only from the VM's subnet

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.


Step 2: Provisioning the VM and Installing Dependencies

I deployed the Ubuntu VM into the public subnet, assigned a public IP, and SSHed in.

Then I installed everything the app needed:

sudo apt update && sudo apt upgrade -y
sudo apt install nodejs npm nginx git mysql-client -y
Enter fullscreen mode Exit fullscreen mode

I verified the installations before moving forward:

node -v
npm -v
git --version
Enter fullscreen mode Exit fullscreen mode

A small habit that saves a lot of debugging later.


Step 3: Deploying the EpicBook Application

I cloned the repository and installed dependencies:

git clone https://github.com/pravinmishraaws/theepicbook.git
cd theepicbook
npm install
Enter fullscreen mode Exit fullscreen mode

Then I configured Nginx as a reverse proxy. The key configuration line that matters for React apps is inside the Nginx server block:

server {
    listen 80;
    server_name _;

    root /home/azureuser/theepicbook/build;
    index index.html;

    location / {
        try_files $uri /index.html;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.


Step 4: Setting Up the Private Database

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:

mysql -h epicbook-db1.mysql.database.azure.com \
      -u admin123 \
      -p bookstore \
      -e "SELECT * FROM author LIMIT 5;"
Enter fullscreen mode Exit fullscreen mode

Seeing the author records return in the terminal confirmed the entire chain was working:

VM → private subnet → MySQL → data ✓
Enter fullscreen mode Exit fullscreen mode

Step 5: Final Security Hardening

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:

sudo ufw status
# Status: inactive
Enter fullscreen mode Exit fullscreen mode

What I Learned

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.


What's Next

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.

Top comments (0)