DEV Community

Ipadeola Taiwo
Ipadeola Taiwo

Posted on • Edited on

I Deployed a Live Website on Azure Using Only the CLI, No Portal, No Clicking

There is a certain kind of confidence you only get when you type a command into a terminal and watch an actual server come to life in another part of the world. No dashboard, no wizard, no clicking through screens. Just you, a terminal, and a set of decisions you have to make yourself.

That is exactly what I set out to do. The brief was simple to read but not simple to execute: deploy a Linux server on Azure, connect to it, configure it, secure it properly, and document the whole process the way a real DevOps engineer would, as a runbook someone else could follow and trust.

I want to walk you through how I did it, the mistakes I made along the way, and how you can follow the same path yourself if you are trying to build this skill.

Why I Did This the Hard Way

It is tempting to open the Azure portal, click a few buttons, and have a server running in minutes. That works, but it does not teach you anything that scales. The moment you need to provision ten servers, or repeat the same setup for a client, clicking through a UI becomes a liability rather than a convenience.

So I chose the terminal instead. Every resource group, every network, every rule was created with a command I typed myself, so that I would actually understand what was happening underneath.

Step One, Giving the Project a Home

Every resource in Azure needs a container to live in, called a resource group. Think of it as the folder that keeps everything related to one project together, so you can manage it, monitor it, or delete it as a single unit later.

az group create --name hagital-rg --location eastus
Enter fullscreen mode Exit fullscreen mode

That one line created a logical home for everything that followed.

Step Two, Building the Network Before the Server

Before you can have a server, you need a network for it to live inside. This is one of those things that feels backwards if you are used to clicking through a portal, where the network gets created for you automatically. On the CLI, you build it yourself, which forces you to actually understand how the pieces fit together.

az network vnet create \
  --resource-group hagital-rg \
  --name provion-server-vnet \
  --address-prefix 10.0.0.0/16 \
  --subnet-name provison-server-subnet \
  --subnet-prefix 10.0.0.0/24
Enter fullscreen mode Exit fullscreen mode

I then double checked everything before moving forward, because catching a mistake here is far easier than catching it after a server is already attached to the wrong network.

az network vnet show \
  --resource-group hagital-rg \
  --name provion-server-vnet \
  --query "{VNet:addressSpace.addressPrefixes, Subnets:subnets[].addressPrefix}"
Enter fullscreen mode Exit fullscreen mode

Step Three, Bringing the Server to Life

With the network ready, I created the virtual machine itself, an Ubuntu server, placed directly inside the network I had just built.

az vm create \
  --resource-group hagital-rg \
  --name hagital-server \
  --image Ubuntu2204 \
  --size Standard_B1ls \
  --vnet-name provion-server-vnet \
  --subnet provison-server-subnet \
  --admin-username azureuser \
  --generate-ssh-keys
Enter fullscreen mode Exit fullscreen mode

One detail worth mentioning here, something that genuinely surprised me. When you create a server through the Azure portal, you are usually handed a private key file to download and guard carefully. On the CLI, the generate-ssh-keys flag handles that for you. Azure generates the key pair locally and wires up the public key automatically, so there is no separate file to lose track of. Small detail, but it changed how I think about secure access.

Step Four, Locking the Front Door, Then Only Opening What Is Needed

A server exposed to the entire internet on every port is a server waiting to be compromised. So the very next thing I did was open only the two ports this server actually needed, port twenty two for SSH access, and port eighty for web traffic. Nothing else.

Opening the first port went smoothly.

az vm open-port --resource-group hagital-rg --name hagital-server --port 22
Enter fullscreen mode Exit fullscreen mode

Opening the second one did not.

az vm open-port --resource-group hagital-rg --name hagital-server --port 80
Enter fullscreen mode Exit fullscreen mode

Azure responded with an error I had never seen before.

SecurityRuleConflict: Security rule open-port-22 conflicts with rule open-port-80. Rules cannot have the same Priority and Direction.
Enter fullscreen mode Exit fullscreen mode

This is the part of the process that actually taught me something. In Azure, every firewall rule, called a network security group rule, has a priority number, and two inbound rules cannot share the same one. The open port command always tries to use the same default priority, nine hundred, no matter how many times you run it. My SSH rule had already claimed that number, so the HTTP rule had nowhere to go.

I checked the existing rules to see exactly what was going on.

az network nsg rule list --resource-group hagital-rg --nsg-name hagital-serverNSG --output table
Enter fullscreen mode Exit fullscreen mode

Once I understood the cause, the fix was straightforward. I created the HTTP rule manually and gave it its own priority instead of relying on the default.

az network nsg rule create \
  --resource-group hagital-rg \
  --nsg-name hagital-serverNSG \
  --name AllowHTTP \
  --priority 910 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --destination-port-ranges 80
Enter fullscreen mode Exit fullscreen mode

That single error taught me more about how Azure firewall rules work than any tutorial had, because I had to actually understand the priority system to solve it myself.

Step Five, Walking In Through SSH

With the network secured, I fetched the server's public IP address and connected directly.

az vm show --resource-group hagital-rg --name hagital-server --show-details --query publicIps --output tsv
Enter fullscreen mode Exit fullscreen mode
ssh azureuser@<public-ip>
Enter fullscreen mode Exit fullscreen mode

No password prompt, no key file to search for, just a clean connection using the key pair Azure had already set up.

Step Six, Giving the Server a Job

A bare server does nothing on its own. I installed Nginx, a lightweight and widely used web server, so the machine could actually serve web pages to anyone who visited its address.

sudo apt update
sudo apt install nginx -y
Enter fullscreen mode Exit fullscreen mode

Running an update before the install is a habit worth keeping. It refreshes the list of available packages so you are installing the current version rather than something outdated sitting in a stale cache.

A quick check confirmed everything was running as expected.

systemctl status nginx
Enter fullscreen mode Exit fullscreen mode

Step Seven, Getting My Own Project Onto the Server

At this point the server could serve a web page, just not mine yet. I copied a portfolio project, built with plain HTML, CSS and JavaScript, from my local machine onto the server using secure copy.

scp -r ./devops_portfolio azureuser@<public-ip>:/tmp/
Enter fullscreen mode Exit fullscreen mode

The next step introduced two more mistakes, both worth sharing because they are easy to make and easy to learn from.

The first attempt tried to move the entire project folder directly onto the location of the existing default web page, and Azure's server refused, since you cannot replace a single file with an entire folder.

The second attempt failed for a different reason, a plain permission error, since the folder that serves web content on a Linux server is owned by the system, not by the everyday user account.

The working solution handled both issues at once, moving the contents of the folder rather than the folder itself, and running the command with elevated permission.

sudo mv /tmp/devops_portfolio/* /var/www/html/
Enter fullscreen mode Exit fullscreen mode

A moment later, visiting the server's public address in a browser showed my actual portfolio, live, served from a machine I had built entirely from a terminal.

Turning the Whole Process Into a Script

Doing this manually taught me the mechanics, but repeating it manually every time would not scale, and would not reflect how this is actually done professionally. So I converted the entire workflow into a bash script, with each stage wrapped in its own function, resource group creation, network creation, server creation, and a reusable function for opening ports safely, one that checks whether a rule already exists before attempting to create it again.

Running the whole provisioning process now takes one command and finishes in a matter of seconds, something that used to take a series of manual steps and careful attention to avoid the exact mistakes I described above.

What This Project Actually Demonstrates

If you are a recruiter or a hiring manager reading this, here is what I want you to take away. This was not a copy and paste exercise. Every command here was typed, tested, and in more than one case, broken and then fixed by understanding the actual cause rather than guessing. I hit a real Azure networking conflict and resolved it by reading the error and understanding how security rule priorities work. I hit a real Linux permissions issue and resolved it the same way.

I also wrote the entire process up as a runbook, the kind of documentation a real engineering team would expect, so that anyone on a team could follow these exact steps and get the same result, safely and predictably.

The live result of this project is a working website, served entirely from infrastructure I provisioned, secured and configured myself, from the terminal, from the ground up.

If you would like to see the full runbook, the automation script, or talk through any part of how this was built, I would genuinely welcome the conversation. This is exactly the kind of work I want to keep doing.

Below is the link to my github repo

GitHub logo highpee1991 / azure-cli-lab

My Azure CLI learning journey from beginner to pro

Day 01 — Azure CLI Basics

What I Learned

Azure CLI is a tool that lets you manage Azure resources from the terminal...

Commands I Ran

Check version

az --version
Enter fullscreen mode Exit fullscreen mode

Output: azure-cli 2.87.0

core 2.87.0 telemetry 1.1.0

Dependencies: msal 1.36.0 azure-mgmt-resource 24.0.0

Python location 'C:\Program Files\Microsoft SDKs\Azure\CLI2\python.exe' Config directory 'C:\Users\Relitorin Orders.azure' Extensions directory 'C:\Users\Relitorin Orders.azure\cliextensions'

Python (Windows) 3.13.13 (tags/v3.13.13:01104ce, Apr 7 2026, 19:25:48) [MSC v.1944 64 bit (AMD64)]

Legal docs and information: aka.ms/AzureCliLegal

Your CLI is up-to-date.

Login to Azure

az login
az account show
Enter fullscreen mode Exit fullscreen mode

Output: Retrieving tenants and subscriptions for the selection...

[Tenant and subscription selection]

No Subscription name Subscription ID Tenant


[1] * Azure subscription 1 Default Directory

The default is marked with an *; the default tenant is 'Default Directory' and subscription is 'Azure subsc ription 1' ().

Select a subscription and tenant (Type a number or Enter for no changes):

Tenant: Default Directory Subscription: Azure subscription…

Top comments (0)