Skip to main content

Azure VM Deployment

This guide walks you through deploying iEMA Bot on an Azure Virtual Machine with a custom domain and HTTPS. By the end, your bot will be accessible at https://your-domain.com with automatic TLS certificates.

Prerequisites

  • An Azure account with an active subscription
  • A domain name you control (e.g., from Namecheap, Cloudflare, GoDaddy, or a university IT department)
  • Completed Installation locally (to verify your surveys and config work before deploying)

1. Create the Azure VM

Via Azure Portal

  1. Go to Virtual MachinesCreateAzure virtual machine.
  2. Configure the basics:
SettingRecommended Value
ImageUbuntu Server 24.04 LTS
SizeStandard_B1s (1 vCPU, 1 GiB RAM) — sufficient for most studies
AuthenticationSSH public key
Usernameazureuser
Inbound portsSSH (22)
  1. Under Disks, the default 30 GB OS disk is sufficient.
  2. Review and create the VM.

Via Azure CLI

az vm create \
--resource-group your-resource-group \
--name iema-bot \
--image Ubuntu2404 \
--size Standard_B1s \
--admin-username azureuser \
--generate-ssh-keys

Open Required Ports

Open HTTP (80) and HTTPS (443) for web traffic:

az vm open-port --resource-group your-resource-group --name iema-bot --port 80 --priority 1001
az vm open-port --resource-group your-resource-group --name iema-bot --port 443 --priority 1002

Or in the portal: VMNetworkingAdd inbound port rule for ports 80 and 443.

2. Point Your Domain to the VM

Get the VM's public IP

az vm show -d --resource-group your-resource-group --name iema-bot --query publicIps -o tsv

Or find it in the portal under the VM's Overview page.

Static IP

By default, Azure assigns a dynamic public IP that can change on VM restart. To prevent this, go to the IP resource and change Assignment from Dynamic to Static, or use the CLI:

az network public-ip update \
--resource-group your-resource-group \
--name iema-botPublicIP \
--allocation-method Static

Configure DNS

Add an A record in your domain's DNS settings:

TypeNameValueTTL
A@ (or subdomain like study)Your VM's public IP300

If using a subdomain (e.g., study.yourdomain.com), set the Name field to study.

Wait for DNS propagation (usually a few minutes, up to 48 hours). Verify with:

nslookup your-domain.com

3. Install Dependencies on the VM

SSH into your VM:

ssh azureuser@your-domain.com

Install Python, uv, and Caddy:

# Update system packages
sudo apt update && sudo apt upgrade -y

# Install Python 3.12
sudo apt install -y python3.12 python3.12-venv

# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc

# Install Caddy (reverse proxy with automatic HTTPS)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy

4. Deploy the Application

Clone the repository and configure:

# Clone
git clone https://github.com/your-org/iema-bot.git ~/iema-bot
cd ~/iema-bot

# Install Python dependencies
uv sync

# Create .env
cp .env.example .env

Edit .env with your production settings:

# Core
BASE_URL=https://your-domain.com
API_KEY=your-secret-api-key

# Telegram (if using Telegram channel)
TELEGRAM_BOT_TOKEN=your-bot-token
ADMIN_USER_IDS=123456789

# Web Push (if using Web App channel)
VAPID_PRIVATE_KEY=...
VAPID_PUBLIC_KEY=...
VAPID_CLAIMS_EMAIL=mailto:researcher@university.edu

# Database
DATABASE_PATH=/home/azureuser/iema-bot/data/esm_bot.db
caution

BASE_URL must be your public HTTPS domain (e.g., https://study.yourdomain.com). This URL is used for cognitive assessment callbacks, wearable OAuth redirects, and webhook subscriptions. If it's wrong, these features will silently fail.

If using the PWA, build it locally and sync the pwa/dist/ directory to the VM (Node.js is not needed on the server):

# On your local machine
cd pwa && npm run build

# Sync to VM
rsync -avz pwa/dist/ azureuser@your-domain.com:~/iema-bot/pwa/dist/

5. Configure Caddy (HTTPS)

Caddy automatically obtains and renews Let's Encrypt TLS certificates — no manual certificate management needed.

Edit the Caddyfile:

sudo nano /etc/caddy/Caddyfile

Replace the contents with:

your-domain.com {
reverse_proxy localhost:8000
}

That's it. Caddy handles:

  • Obtaining a TLS certificate from Let's Encrypt
  • Redirecting HTTP → HTTPS
  • Renewing certificates automatically
  • Proxying all traffic to the bot on port 8000
  • WebSocket upgrade for PWA connections

Restart Caddy to apply:

sudo systemctl restart caddy

Verify it's running:

sudo systemctl status caddy
Multiple subdomains

If you run multiple studies on the same VM, Caddy handles each with its own certificate:

study-a.yourdomain.com {
reverse_proxy localhost:8000
}

study-b.yourdomain.com {
reverse_proxy localhost:8001
}

6. Set Up the Systemd Service

Create a service file so the bot starts automatically and restarts on failure:

sudo nano /etc/systemd/system/esm-bot.service
[Unit]
Description=ESM/EMA Bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=azureuser
WorkingDirectory=/home/azureuser/iema-bot
EnvironmentFile=/home/azureuser/iema-bot/.env
ExecStart=/home/azureuser/.local/bin/uv run python -m src.main
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable esm-bot
sudo systemctl start esm-bot

7. Verify the Deployment

Check that everything is running:

# Bot service
sudo systemctl status esm-bot

# Caddy reverse proxy
sudo systemctl status caddy

# Bot logs
sudo journalctl -u esm-bot -f

Test the public endpoints:

# Health check
curl https://your-domain.com/api/v1/health

# API (authenticated)
curl -H "Authorization: Bearer your-secret-api-key" \
https://your-domain.com/api/v1/participants

If using the PWA, open https://your-domain.com/pwa/ in a browser.

Common Operations

View logs

sudo journalctl -u esm-bot -f          # Follow live logs
sudo journalctl -u esm-bot -n 100 # Last 100 lines
sudo journalctl -u esm-bot --since today

Restart the bot

sudo systemctl restart esm-bot

Update the deployment

cd ~/iema-bot
git pull
uv sync
sudo systemctl restart esm-bot

Back up the database

# SQLite safe backup (while the bot is running)
sqlite3 /home/azureuser/iema-bot/data/esm_bot.db ".backup '/home/azureuser/backups/esm_bot_$(date +%Y%m%d).db'"

Troubleshooting

SymptomCheck
HTTPS not workingVerify ports 80/443 are open in Azure NSG. Check sudo systemctl status caddy and sudo journalctl -u caddy.
DNS not resolvingVerify the A record points to the correct IP. Try nslookup your-domain.com. DNS can take up to 48 hours to propagate.
Bot not startingCheck sudo journalctl -u esm-bot -n 50. Common issues: missing .env variables, wrong Python version.
Cognitive assessments not completingVerify BASE_URL in .env matches your public HTTPS domain exactly. Assessment callbacks POST to {BASE_URL}/api/v1/cognitive/complete.
Wearable OAuth failingSame as above — OAuth redirect URIs use BASE_URL. Also check that the OAuth credentials are set in .env.
WebSocket disconnectingCaddy handles WebSocket upgrade automatically. If using a CDN or load balancer in front, ensure WebSocket support is enabled.