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
- Go to Virtual Machines → Create → Azure virtual machine.
- Configure the basics:
| Setting | Recommended Value |
|---|---|
| Image | Ubuntu Server 24.04 LTS |
| Size | Standard_B1s (1 vCPU, 1 GiB RAM) — sufficient for most studies |
| Authentication | SSH public key |
| Username | azureuser |
| Inbound ports | SSH (22) |
- Under Disks, the default 30 GB OS disk is sufficient.
- 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: VM → Networking → Add 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.
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:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | @ (or subdomain like study) | Your VM's public IP | 300 |
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
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
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
| Symptom | Check |
|---|---|
| HTTPS not working | Verify ports 80/443 are open in Azure NSG. Check sudo systemctl status caddy and sudo journalctl -u caddy. |
| DNS not resolving | Verify the A record points to the correct IP. Try nslookup your-domain.com. DNS can take up to 48 hours to propagate. |
| Bot not starting | Check sudo journalctl -u esm-bot -n 50. Common issues: missing .env variables, wrong Python version. |
| Cognitive assessments not completing | Verify BASE_URL in .env matches your public HTTPS domain exactly. Assessment callbacks POST to {BASE_URL}/api/v1/cognitive/complete. |
| Wearable OAuth failing | Same as above — OAuth redirect URIs use BASE_URL. Also check that the OAuth credentials are set in .env. |
| WebSocket disconnecting | Caddy handles WebSocket upgrade automatically. If using a CDN or load balancer in front, ensure WebSocket support is enabled. |