Complete guide to deploy your ML project to Azure Virtual Machine with Azure Container Registry (ACR) and GitHub Actions CI/CD.
- Azure Account with active subscription (Get free $200 credit)
- GitHub repository with your code
- Basic familiarity with Azure Portal
- Sign in to Azure Portal: https://portal.azure.com/
- Click the Cloud Shell icon (>_) in top right
- Select Bash when prompted
- Wait for Cloud Shell to initialize
Run these commands in Cloud Shell:
# Get your subscription ID
az account show --query id --output tsv
# Create service principal with contributor role
# Replace YOUR_SUBSCRIPTION_ID with the ID from above
az ad sp create-for-rbac \
--name "github-actions-mlproject" \
--role contributor \
--scopes /subscriptions/YOUR_SUBSCRIPTION_ID \
--sdk-authIMPORTANT: Copy the entire JSON output. You'll need this for GitHub Secrets.
Example output:
{
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"subscriptionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
"resourceManagerEndpointUrl": "https://management.azure.com/",
...
}- In Azure Portal, search for Resource groups
- Click + Create
- Subscription: Select your subscription
- Resource group:
mlproject-rg - Region:
East US(or choose closer region) - Click Review + create → Create
az group create --name mlproject-rg --location eastus- Search for Container registries
- Click + Create
- Subscription: Select your subscription
- Resource group: Select
mlproject-rg - Registry name:
mlprojectacr(must be globally unique, lowercase, alphanumeric) - Location: Same as resource group (
East US) - SKU: Basic (sufficient for small projects)
- Click Review + create → Create
- Wait 1-2 minutes for deployment
- Go to your ACR resource
- From the Overview page, copy:
- Login server:
mlprojectacr.azurecr.io→ This is your ACR_LOGIN_SERVER - Registry name:
mlprojectacr→ This is your ACR_NAME
- Login server:
- In your ACR, click Access keys in left menu
- Enable Admin user toggle
- Copy and save:
- Username: Usually same as registry name
- password: One of the two passwords shown
- You'll use these for Docker login
- Search for Virtual machines
- Click + Create → Azure virtual machine
- Subscription: Select your subscription
- Resource group: Select
mlproject-rg - Virtual machine name:
mlproject-vm - Region: Same as resource group (
East US) - Availability options: No infrastructure redundancy required
- Security type: Standard
- Image: Ubuntu Server 22.04 LTS - x64 Gen2
- Size: Click See all sizes
- Select Standard_B2s (2 vCPUs, 4GB RAM) - Good for ML
- Or Standard_B1s (1 vCPU, 1GB RAM) for testing only
- Authentication type: SSH public key
- Username:
azureuser - SSH public key source: Generate new key pair
- Key pair name:
mlproject-vm_key
- Public inbound ports: Select Allow selected ports
- Select inbound ports: Check both:
- SSH (22)
- HTTP (80)
- Click Next: Disks
- OS disk type: Standard SSD (cost-effective)
- Delete with VM: Check this box
- Click Next: Networking
- Virtual network: (new) mlproject-vm-vnet (default is fine)
- Subnet: (new) default (10.0.0.0/24)
- Public IP: (new) mlproject-vm-ip
- NIC network security group: Basic
- Public inbound ports: Confirm SSH (22), HTTP (80) are selected
- Delete public IP and NIC when VM is deleted: Check this box
- Click Review + create
- Review the configuration
- Click Create
IMPORTANT: A popup will appear asking you to download the private key.
-
Click Download private key and create resource
-
File downloads as
mlproject-vm_key.pem -
Save this file securely! You can't download it again.
-
Move to safe location:
# On Windows (PowerShell): Move-Item ~/Downloads/mlproject-vm_key.pem ~/.ssh/
# On Mac/Linux: mv ~/Downloads/mlproject-vm_key.pem ~/.ssh/ chmod 400 ~/.ssh/mlproject-vm_key.pem
- Wait 3-5 minutes for VM creation
- Click Go to resource when complete
From the VM Overview page, copy and save:
- Public IP address (e.g.,
20.185.45.123) → This is VM_HOST - Size: Confirm it's the size you selected
- Status: Should show "Running"
# On Windows PowerShell:
ssh -i ~/.ssh/mlproject-vm_key.pem azureuser@20.185.45.123
# On Mac/Linux:
ssh -i ~/.ssh/mlproject-vm_key.pem azureuser@YOUR_VM_IPType yes when asked about fingerprint.
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Start Docker service
sudo systemctl start docker
sudo systemctl enable docker
# Add user to docker group (no sudo needed for docker commands)
sudo usermod -aG docker azureuser
# Install Docker Compose (optional but useful)
sudo apt install docker-compose -y
# Log out and back in for group changes to take effect
exit
# Then SSH back in# After reconnecting
docker --version
docker ps# Install Azure CLI
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Verify installation
az --versionOption A - Using Admin Credentials (Simpler):
# Login to ACR using admin credentials
docker login mlprojectacr.azurecr.io
# Enter:
# Username: mlprojectacr (your ACR name)
# Password: (use the password from Step 3.3)Option B - Using Service Principal (Production Recommended):
# Login using service principal
az login --service-principal \
--username YOUR_CLIENT_ID \
--password YOUR_CLIENT_SECRET \
--tenant YOUR_TENANT_ID
# Login to ACR
az acr login --name mlprojectacr# This should show "Login Succeeded"
docker login mlprojectacr.azurecr.io- In Azure Portal, go to your VM
- Click Networking in left menu
- Confirm these inbound rules exist:
- Port 22 (SSH) from your IP or Any
- Port 80 (HTTP) from Any (0.0.0.0/0)
If your app runs on a custom port (e.g., 8080):
- Click Add inbound port rule
- Destination port ranges:
8080 - Protocol: TCP
- Action: Allow
- Name:
Port_8080 - Click Add
- Go to your GitHub repository
- Click Settings tab
- In left sidebar, click Secrets and variables → Actions
- Click New repository secret for each below
Add these secrets one by one:
| Secret Name | Value | Where to Find |
|---|---|---|
AZURE_CREDENTIALS |
Full JSON output from Step 1.2 | The entire JSON from service principal creation |
AZURE_SUBSCRIPTION_ID |
Your subscription ID | From the JSON or Azure Portal |
ACR_LOGIN_SERVER |
mlprojectacr.azurecr.io |
From Step 3.2 (your ACR login server) |
ACR_NAME |
mlprojectacr |
Your ACR name from Step 3 |
ACR_USERNAME |
Admin username | From Step 3.3 (usually same as ACR name) |
ACR_PASSWORD |
Admin password | From Step 3.3 (ACR access keys) |
VM_HOST |
20.185.45.123 |
From Step 4.4 (VM public IP) |
VM_USER |
azureuser |
Default Azure VM username |
VM_SSH_KEY |
Full content of .pem file | See Step 7.3 below |
On Windows (PowerShell):
Get-Content ~/.ssh/mlproject-vm_key.pem | clipOn Mac/Linux:
cat ~/.ssh/mlproject-vm_key.pem | pbcopy # Mac
# OR
cat ~/.ssh/mlproject-vm_key.pem # Copy output manuallyPaste the ENTIRE content (including -----BEGIN OPENSSH PRIVATE KEY----- and -----END OPENSSH PRIVATE KEY-----) into the VM_SSH_KEY secret.
Create .github/workflows/azure-deploy.yml:
# CI/CD Pipeline: Build, Push to ACR, Deploy to Azure VM
name: CI/CD - Deploy to Azure
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
ACR_LOGIN_SERVER: ${{ secrets.ACR_LOGIN_SERVER }}
ACR_NAME: ${{ secrets.ACR_NAME }}
IMAGE_NAME: mlproject
permissions:
contents: read
jobs:
# ===== CI: Run tests and validation =====
ci:
name: Continuous Integration
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8 (optional)
continue-on-error: true
run: |
pip install flake8
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics || true
- name: Run tests (if present)
continue-on-error: true
run: |
if [ -d "tests" ] || ls *test*.py 2>/dev/null; then
pip install pytest
pytest -v || true
else
echo "No tests found, skipping test step"
fi
# ===== BUILD: Build Docker image and push to ACR =====
build-and-push:
name: Build and Push to ACR
runs-on: ubuntu-latest
needs: ci
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
outputs:
image: ${{ steps.build-image.outputs.image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Login to Azure Container Registry
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.ACR_LOGIN_SERVER }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build, tag, and push image to ACR
id: build-image
env:
IMAGE_TAG: ${{ github.sha }}
run: |
# Build Docker image
docker build -t $ACR_LOGIN_SERVER/$IMAGE_NAME:$IMAGE_TAG .
docker tag $ACR_LOGIN_SERVER/$IMAGE_NAME:$IMAGE_TAG $ACR_LOGIN_SERVER/$IMAGE_NAME:latest
# Push both tags to ACR
docker push $ACR_LOGIN_SERVER/$IMAGE_NAME:$IMAGE_TAG
docker push $ACR_LOGIN_SERVER/$IMAGE_NAME:latest
echo "image=$ACR_LOGIN_SERVER/$IMAGE_NAME:$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "✅ Image pushed: $ACR_LOGIN_SERVER/$IMAGE_NAME:$IMAGE_TAG"
# ===== DEPLOY: Deploy to Azure VM =====
deploy:
name: Deploy to Azure VM
runs-on: ubuntu-latest
needs: build-and-push
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.VM_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.VM_HOST }} >> ~/.ssh/known_hosts
- name: Deploy to Azure VM
env:
VM_HOST: ${{ secrets.VM_HOST }}
VM_USER: ${{ secrets.VM_USER }}
ACR_LOGIN_SERVER: ${{ secrets.ACR_LOGIN_SERVER }}
ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
IMAGE_NAME: ${{ env.IMAGE_NAME }}
run: |
ssh -i ~/.ssh/id_rsa $VM_USER@$VM_HOST << 'ENDSSH'
set -e
echo "🔐 Logging into ACR..."
echo "${{ secrets.ACR_PASSWORD }}" | docker login ${{ secrets.ACR_LOGIN_SERVER }} \
--username ${{ secrets.ACR_USERNAME }} \
--password-stdin
echo "🐳 Pulling latest image..."
docker pull ${{ secrets.ACR_LOGIN_SERVER }}/${{ env.IMAGE_NAME }}:latest
echo "🛑 Stopping old container (if exists)..."
docker stop mlproject 2>/dev/null || true
docker rm mlproject 2>/dev/null || true
echo "🚀 Starting new container..."
docker run -d \
--name mlproject \
--restart unless-stopped \
-p 80:8080 \
${{ secrets.ACR_LOGIN_SERVER }}/${{ env.IMAGE_NAME }}:latest
echo "🧹 Cleaning up old images..."
docker image prune -af
echo "✅ Deployment completed!"
docker ps | grep mlproject
ENDSSH
- name: Verify Deployment
run: |
echo "🌐 Application URL: http://${{ secrets.VM_HOST }}/"
echo "✅ Deployment pipeline completed successfully!"git add .github/workflows/azure-deploy.yml
git commit -m "Add Azure CI/CD pipeline"
git push origin main- Go to your GitHub repository
- Click Actions tab
- You should see a workflow running: CI/CD - Deploy to Azure
- Click on the running workflow to see live logs
- ✅ CI: Tests and linting (2-3 min)
- ✅ Build and Push to ACR: Docker build and push (5-10 min)
- ✅ Deploy to Azure VM: SSH and container deployment (2-3 min)
Open browser and go to:
http://YOUR_VM_PUBLIC_IP/
You should see your ML project homepage!
http://YOUR_VM_PUBLIC_IP/predictdata
# SSH into VM
ssh -i ~/.ssh/mlproject-vm_key.pem azureuser@YOUR_VM_IP
# Check running containers
docker ps
# View logs
docker logs mlproject -f
# Check recent logs
docker logs --tail 50 mlproject- In Azure Portal, go to your VM
- Click Networking → Public IP address link
- Click Configuration in left menu
- Assignment: Change to Static
- Click Save
- Go to your domain registrar (Namecheap, GoDaddy, etc.)
- Create A Record:
- Name:
mlprojector@(for root domain) - Type: A
- Value: Your VM static public IP
- TTL: 300 or Auto
- Name:
# SSH into VM
ssh -i ~/.ssh/mlproject-vm_key.pem azureuser@YOUR_VM_IP
# Install Nginx
sudo apt update
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
# Install Certbot for Let's Encrypt SSL
sudo apt install certbot python3-certbot-nginx -y
# Get SSL certificate
sudo certbot --nginx -d mlproject.yourdomain.com
# Configure Nginx as reverse proxy
sudo nano /etc/nginx/sites-available/mlprojectAdd this configuration:
server {
listen 80;
server_name mlproject.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name mlproject.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mlproject.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mlproject.yourdomain.com/privkey.pem;
location / {
proxy_pass http://localhost:80;
proxy_set_header Host $host;
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;
}
}# Enable the site
sudo ln -s /etc/nginx/sites-available/mlproject /etc/nginx/sites-enabled/
# Test and reload Nginx
sudo nginx -t
sudo systemctl reload nginx- In Azure Portal, go to VM → Networking
- Ensure HTTPS (443) port is open
- If not, click Add inbound port rule:
- Destination port ranges:
443 - Protocol: TCP
- Name:
HTTPS - Click Add
- Destination port ranges:
- Check
VM_SSH_KEYsecret contains full private key with BEGIN/END lines - Verify
VM_USERisazureuser - Check VM Network Security Group allows SSH from 0.0.0.0/0
- SSH into VM and test:
docker login mlprojectacr.azurecr.io - Verify ACR admin user is enabled
- Check ACR username and password in GitHub Secrets
- Docker not installed on VM
- Reconnect SSH after adding user to docker group
- Check Network Security Group allows HTTP (port 80)
- Verify container is running:
docker ps - Check container logs:
docker logs mlproject - Test locally on VM:
curl http://localhost
- View logs:
docker logs mlproject - Check if
artifacts/folder exists with models - Verify Dockerfile and application.py are correct
- Stop VM when not in use: VM → Stop (you won't be charged for compute)
- Delete old ACR images: Go to ACR → Repositories → Select old tags → Delete
- Use smaller VM size: B1s instead of B2s for testing
- Set up auto-shutdown: VM → Auto-shutdown (schedule daily shutdown)
- ACR Basic: ~$5/month
- VM B2s (2 vCPU, 4GB RAM): ~$30/month (730 hours)
- VM B1s (1 vCPU, 1GB RAM): ~$8/month
- Standard SSD (30GB): ~$2.50/month
- Public IP (Static): ~$3.60/month
- Total: ~$40-45/month for B2s, ~$20/month for B1s
Save money:
- Use B1s for development/testing
- Stop VM when not needed (VM → Stop)
- Use Azure Free Tier (12 months: 750 hours/month B1s)
- Delete unused ACR images regularly
- In Azure Portal, go to your VM
- Click Auto-shutdown in left menu
- Enable: Toggle ON
- Scheduled shutdown time: 11:00 PM (or your preferred time)
- Time zone: Select your timezone
- Notification: Add your email (optional)
- Click Save
This saves money by automatically stopping VM at night!
# SSH to Azure VM
ssh -i ~/.ssh/mlproject-vm_key.pem azureuser@YOUR_VM_IP
# Check running containers
docker ps
# View logs
docker logs -f mlproject
# Restart container
docker restart mlproject
# Pull and run manually
docker login mlprojectacr.azurecr.io
docker pull mlprojectacr.azurecr.io/mlproject:latest
docker stop mlproject && docker rm mlproject
docker run -d --name mlproject -p 80:8080 mlprojectacr.azurecr.io/mlproject:latest
# Check disk space
df -h
# Check system resources
htop # install first: sudo apt install htop -y
# View Azure CLI login status
az account show
# Stop/Start VM from CLI
az vm stop --resource-group mlproject-rg --name mlproject-vm
az vm start --resource-group mlproject-rg --name mlproject-vm
# Delete resource group (remove everything)
az group delete --name mlproject-rg --yes --no-wait- Azure Monitor: Set up alerts for VM health and performance
- Azure Application Insights: Monitor application performance
- Azure Key Vault: Store secrets securely
- Azure DevOps: Alternative to GitHub Actions
- Azure Load Balancer: Scale with multiple VMs
- Azure App Service: Simpler PaaS alternative to VMs
- Azure Container Instances: Serverless containers
| Feature | AWS | Azure |
|---|---|---|
| Container Registry | ECR | ACR |
| Virtual Machine | EC2 | Azure VM |
| CLI Tool | AWS CLI | Azure CLI |
| Auth Method | IAM User/Keys | Service Principal |
| Default User | ec2-user/ubuntu | azureuser |
| Free Tier | 750hrs t2.micro | 750hrs B1s |
| Networking | Security Groups | Network Security Groups |
- ✅ Monitor GitHub Actions on every push
- ✅ Set up Azure Monitor alerts
- ✅ Configure automated backups
- ✅ Implement blue-green deployments with staging slots
- ✅ Explore Azure App Service as PaaS alternative
- ✅ Add Azure Application Insights for monitoring
If you encounter issues:
- Check GitHub Actions logs
- SSH into VM and check
docker logs mlproject - Verify all GitHub Secrets are correct
- Check Azure Portal → VM → Activity log for Azure-specific errors
- Review Azure Monitor logs (if enabled)
Good luck with your Azure deployment! ☁️🚀