This guide provides step-by-step instructions for deploying your Next.js application to an Azure Virtual Machine without using Docker. The application uses PostgreSQL database, Prisma ORM, and requires proper environment configuration.
- Azure subscription
- Domain name (optional but recommended for production)
- SSH client (OpenSSH, PuTTY, etc.)
- Git repository access
-
Navigate to Virtual Machines:
- Go to Azure Portal
- Search for "Virtual machines" and click "Create"
-
Configure Basic Settings:
- Subscription: Select your subscription
- Resource group: Create new or select existing (e.g.,
hydraa-rg) - Virtual machine name:
hydraa-vm - Region: Choose a region close to your users (e.g., East US, West Europe)
- Availability options: No infrastructure redundancy required
- Security type: Standard
- Image: Ubuntu Server 22.04 LTS - Gen2
- VM architecture: x64
- Size: Standard_B2s (2 vCPUs, 4 GB RAM) - minimum recommended
- For production with moderate traffic: Standard_B4ms (4 vCPUs, 16 GB RAM)
- For high traffic: Standard_D4s_v5 or higher
-
Configure Administrator Account:
- Authentication type: SSH public key
- Username:
hydraauser - SSH public key source: Generate new key pair
- Key pair name:
hydraa-vm-key
-
Configure Networking:
- Virtual network: Create new or select existing
- Subnet: default
- Public IP: Create new
- NIC network security group: Advanced
- Configure network security group:
- Add inbound security rules:
- SSH (port 22) - Source: Your IP address only
- HTTP (port 80) - Source: Any
- HTTPS (port 443) - Source: Any
- Add inbound security rules:
-
Configure Management:
- Boot diagnostics: Disable (optional)
- Identity: System assigned managed identity (optional, for advanced scenarios)
-
Review and Create:
- Review configuration
- Click "Create"
- Download the private key (.pem file) when prompted
# Install Azure CLI if not already installed
# Windows: winget install Microsoft.AzureCLI
# macOS: brew install azure-cli
# Linux: curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Login to Azure
az login
# Create resource group
az group create --name hydraa-rg --location eastus
# Create VM with SSH key authentication
az vm create \
--resource-group hydraa-rg \
--name hydraa-vm \
--image Ubuntu2204 \
--size Standard_B2s \
--admin-username hydraauser \
--generate-ssh-keys \
--public-ip-sku Standard \
--nsg hydraa-nsg
# Configure NSG rules
az network nsg rule create \
--resource-group hydraa-rg \
--nsg-name hydraa-nsg \
--name AllowHTTP \
--priority 100 \
--destination-port-ranges 80 \
--access Allow \
--protocol Tcp
az network nsg rule create \
--resource-group hydraa-rg \
--nsg-name hydraa-nsg \
--name AllowHTTPS \
--priority 101 \
--destination-port-ranges 443 \
--access Allow \
--protocol Tcp
# Get VM public IP
VM_IP=$(az vm show -d -g hydraa-rg -n hydraa-vm --query publicIps -o tsv)
echo "VM Public IP: $VM_IP"# Set correct permissions for private key (if using key-based auth)
chmod 600 ~/Downloads/hydraa-vm-key.pem
# Connect to VM
ssh -i ~/Downloads/hydraa-vm-key.pem hydraauser@<VM_PUBLIC_IP># Update system packages
sudo apt update && sudo apt upgrade -y
# Install essential packages
sudo apt install -y curl wget git htop ufw software-properties-common
# Install Node.js 20.x (as specified in package.json)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verify Node.js installation
node --version
npm --version
# Install PM2 globally for process management
sudo npm install -g pm2
# Install PostgreSQL
sudo apt install -y postgresql postgresql-contrib
# Install nginx
sudo apt install -y nginx
# Install certbot for SSL certificates
sudo apt install -y certbot python3-certbot-nginx
# Install build essentials (for native modules)
sudo apt install -y build-essential
# Clean up
sudo apt autoremove -y# Switch to postgres user
sudo -u postgres psql
# Inside PostgreSQL shell, create database and user
CREATE DATABASE hydraa;
CREATE USER hydraa_user WITH ENCRYPTED PASSWORD 'hydraa_password';
GRANT ALL PRIVILEGES ON DATABASE hydraa TO hydraa_user;
ALTER USER hydraa_user CREATEDB;
# Grant schema privileges (required for Prisma migrations)
GRANT ALL ON SCHEMA public TO hydraa_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO hydraa_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO hydraa_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO hydraa_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO hydraa_user;
\q
# Create database directory for backups
sudo mkdir -p /var/backups/postgresql
sudo chown postgres:postgres /var/backups/postgresql# Enable UFW
sudo ufw enable
# Allow necessary ports
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
# Check status
sudo ufw status# Navigate to home directory
cd ~
# Clone your repository (replace with your actual repo URL)
git clone https://github.com/yourusername/your-repo-name.git hydraa
cd hydraa
# If using private repo, you'll need to configure SSH keys or use HTTPS with token# Install npm dependencies
npm install
# Generate Prisma client
npx prisma generate# Copy environment example file
cp .env.example .env.production
# Edit production environment file
nano .env.productionUpdate the following variables in .env.production:
# Application
NODE_ENV=production
SECRET=your-super-secure-random-secret-key-here
# URLs
NEXTAUTH_URL=https://hydraa.eastasia.cloudapp.azure.com
NEXT_PUBLIC_SITE_URL=https://hydraa.eastasia.cloudapp.azure.com
SITE_URL=https://hydraa.eastasia.cloudapp.azure.com
# Database
DATABASE_URL=postgresql://hydraa_user:your_strong_password_here@localhost:5432/hydraa
# OAuth Providers (configure as needed)
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Email (configure SMTP settings)
EMAIL_SERVER_HOST=smtp.gmail.com
EMAIL_SERVER_PORT=587
EMAIL_SERVER_USER=your_email@gmail.com
EMAIL_SERVER_PASSWORD=your_app_password
EMAIL_FROM=your_email@gmail.com
# Stripe (if using payments)
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Other API keys as neededSecurity Note: Generate a strong secret key:
openssl rand -base64 32# Option 1: If you have migration files, deploy them
npx prisma migrate deploy
# Option 2: If no migrations exist, push schema directly to database (recommended for production)
npx prisma db push
# Verify database connection and schema
npx prisma db pull
# Seed database (if you have seed script)
npx prisma db seed# Build the Next.js application
npm run build
# Verify build
ls -la .next/# Create ecosystem file for PM2
nano ecosystem.config.jsAdd the following content:
module.exports = {
apps: [{
name: 'hydraa',
script: 'npm',
args: 'start',
cwd: '/home/hydraauser/hydraa',
env: {
NODE_ENV: 'production'
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
instances: 1,
exec_mode: 'fork',
max_memory_restart: '1G',
error_file: '/home/hydraauser/hydraa/logs/app-error.log',
out_file: '/home/hydraauser/hydraa/logs/app-out.log',
log_file: '/home/hydraauser/hydraa/logs/app.log',
time: true,
watch: false,
autorestart: true,
max_restarts: 5,
min_uptime: '10s'
}]
};mkdir -p ~/hydraa/logs# Start the application
pm2 start ecosystem.config.js --env production
# Save PM2 configuration
pm2 save
# Generate startup script
pm2 startup
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u hydraauser --hp /home/hydraauser
# Check status
pm2 status
pm2 logs hydraa --lines 50sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.backupsudo nano /etc/nginx/sites-available/hydraaAdd the following configuration (replace yourdomain.com with your actual domain):
server {
listen 80;
server_name hydraa.eastasia.cloudapp.azure.com www.hydraa.eastasia.cloudapp.azure.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name hydraa.eastasia.cloudapp.azure.com www.hydraa.eastasia.cloudapp.azure.com;
# SSL configuration (certbot will add these)
ssl_certificate /etc/letsencrypt/live/hydraa.eastasia.cloudapp.azure.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hydraa.eastasia.cloudapp.azure.com/privkey.pem;
# SSL security settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
# Client max body size for file uploads
client_max_body_size 100M;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# Main application proxy
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
proxy_buffering off;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# API rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
# Auth endpoints rate limiting
location /api/auth/ {
limit_req zone=login burst=10 nodelay;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
# Static files caching
location /_next/static/ {
proxy_pass http://127.0.0.1:3000;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Logging
access_log /var/log/nginx/hydraa_access.log;
error_log /var/log/nginx/hydraa_error.log;
# Block certain file types
location ~* \.(htaccess|htpasswd|ini|log|sh|sql|conf)$ {
deny all;
}
# Block access to hidden files
location ~ /\. {
deny all;
}
}# Disable default site
sudo unlink /etc/nginx/sites-enabled/default
# Enable hydraa site
sudo ln -s /etc/nginx/sites-available/hydraa /etc/nginx/sites-enabled/
# Test nginx configuration
sudo nginx -t
# Restart nginx
sudo systemctl restart nginx# Replace yourdomain.com with your actual domain
sudo certbot --nginx -d hydraa.eastasia.cloudapp.azure.com -d www.hydraa.eastasia.cloudapp.azure.com
# Test automatic renewal
sudo certbot renew --dry-runSSL certificates from Let's Encrypt expire every 90 days. The renewal is automatic, but you can set up monitoring:
# Check certificate status
sudo certbot certificates
# Force renewal (if needed)
sudo certbot renew --force-renewal-
Purchase Domain: If you don't have one, purchase from Namecheap, GoDaddy, etc.
-
Configure DNS:
- A Record:
hydraa.eastasia.cloudapp.azure.com→<VM_PUBLIC_IP> - CNAME Record:
www.hydraa.eastasia.cloudapp.azure.com→hydraa.eastasia.cloudapp.azure.com
- A Record:
-
Wait for Propagation: DNS changes can take up to 24-48 hours to propagate globally.
-
Verify Domain: Once DNS is propagated, run certbot again if needed.
# Edit SSH configuration
sudo nano /etc/ssh/sshd_config
# Make these changes:
# Port 22 # Change to non-standard port (optional)
# PermitRootLogin no
# PasswordAuthentication no # If using key-based auth only
# AllowUsers azureuser
# Restart SSH
sudo systemctl restart sshd# Install fail2ban
sudo apt install -y fail2ban
# Configure for SSH
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
# Enable SSH jail
# [sshd]
# enabled = true
# Restart fail2ban
sudo systemctl restart fail2ban# Secure environment file
chmod 600 ~/hydraa/.env.production
# Secure SSH keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys# Create backup script
nano ~/backup.sh#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/var/backups/postgresql"
DB_NAME="hydraa"
DB_USER="hydraa_user"
# Create database backup
sudo -u postgres pg_dump -U $DB_USER $DB_NAME > "$BACKUP_DIR/db_backup_$DATE.sql"
# Compress backup
gzip "$BACKUP_DIR/db_backup_$DATE.sql"
# Keep only last 7 days of backups
find $BACKUP_DIR -name "db_backup_*.sql.gz" -mtime +7 -delete
echo "Backup completed: db_backup_$DATE.sql.gz"# Make executable
chmod +x ~/backup.sh
# Test backup
./backup.sh
# Add to crontab for daily backups at 2 AM
crontab -e
# Add: 0 2 * * * /home/azureuser/backup.sh# Create application backup script
nano ~/backup-app.sh#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/home/azureuser/backups"
APP_DIR="/home/azureuser/hydraa"
# Create backup directory
mkdir -p $BACKUP_DIR
# Backup application files (excluding node_modules and .next)
tar -czf "$BACKUP_DIR/app_backup_$DATE.tar.gz" \
--exclude="$APP_DIR/node_modules" \
--exclude="$APP_DIR/.next" \
--exclude="$APP_DIR/logs" \
$APP_DIR
echo "Application backup completed: app_backup_$DATE.tar.gz"# Check PM2 status
pm2 status
# View application logs
pm2 logs hydraa
pm2 logs hydraa --lines 100
# Monitor resources
htop
df -h
free -h# Check nginx status
sudo systemctl status nginx
# View nginx logs
sudo tail -f /var/log/nginx/hydraa_access.log
sudo tail -f /var/log/nginx/hydraa_error.log# Install monitoring tools
sudo apt install -y iotop sysstat
# Check system logs
sudo journalctl -u pm2-azureuser -f
sudo journalctl -u nginx -f# Configure log rotation for application logs
sudo nano /etc/logrotate.d/hydraa/home/azureuser/hydraa/logs/*.log {
daily
missingok
rotate 30
compress
notifempty
create 0644 azureuser azureuser
postrotate
pm2 reloadLogs
endscript
}
# Navigate to application directory
cd ~/hydraa
# Pull latest changes
git pull origin main
# Install any new dependencies
npm install
# Generate Prisma client (if schema changed)
npx prisma generate
# Run migrations (if any)
npx prisma migrate deploy
# Build application
npm run build
# Restart application
pm2 restart hydraa
# Check status
pm2 status
curl -I http://localhost:3000For zero-downtime deployments, you can use PM2's cluster mode:
// Update ecosystem.config.js
module.exports = {
apps: [{
name: 'hydraa',
script: 'npm',
args: 'start',
cwd: '/home/azureuser/hydraa',
instances: 2, // Number of instances
exec_mode: 'cluster', // Cluster mode
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
// ... other config
}]
};Then reload with:
pm2 reload ecosystem.config.jsApplication not starting:
pm2 logs hydraa
pm2 restart hydraaDatabase connection issues:
# Check PostgreSQL status
sudo systemctl status postgresql
# Test database connection
psql -h localhost -U hydraa_user -d hydraanginx configuration errors:
sudo nginx -t
sudo systemctl restart nginxSSL certificate issues:
sudo certbot certificates
sudo certbot renew --force-renewalPort already in use:
sudo netstat -tulpn | grep :80
sudo netstat -tulpn | grep :443
sudo netstat -tulpn | grep :3000Memory issues:
free -h
pm2 monitPM2 Configuration Tuning:
// ecosystem.config.js optimizations
module.exports = {
apps: [{
// ... existing config
max_memory_restart: '800M',
node_args: '--max-old-space-size=4096',
instances: 'max', // Use all CPU cores
exec_mode: 'cluster'
}]
};Nginx Optimizations:
# Add to nginx config
worker_processes auto;
worker_connections 1024;PostgreSQL Tuning:
# Edit postgresql.conf
sudo nano /etc/postgresql/14/main/postgresql.conf
# Adjust settings based on your VM size:
# shared_buffers = 256MB
# effective_cache_size = 1GB
# work_mem = 4MB
# Restart PostgreSQL
sudo systemctl restart postgresql- Right-size your VM: Monitor usage and adjust VM size as needed
- Use Reserved Instances: For long-term deployments
- Auto-shutdown: Configure auto-shutdown during off-hours
- Monitor costs: Use Azure Cost Management
- Connection pooling: Consider using PgBouncer
- Query optimization: Monitor slow queries
- Index optimization: Regularly review and optimize indexes
- Increase VM size through Azure Portal
- Adjust PostgreSQL memory settings
- Update PM2 configuration for more instances
- Load balancer setup
- Database read replicas
- Redis for session storage
- CDN for static assets
This guide covers the complete deployment process from VM creation to production deployment. Key components include:
- Azure VM setup with proper security
- Node.js and PostgreSQL installation
- Application deployment with PM2
- Nginx reverse proxy configuration
- SSL certificate setup
- Security hardening
- Backup and monitoring strategies
- Troubleshooting and optimization tips
For production environments, consider implementing additional monitoring, automated testing, and disaster recovery procedures. c:\Users\Jayakumar\Documents\hydraa\hydraa\hydranew\hydraa\AZURE_VM_DEPLOYMENT_GUIDE.md