Deploy your personal AI assistant to Azure Container Apps with Discord integration. This sample shows how to run MoltBot - an open-source personal AI assistant - on Azure's serverless container platform.
- 🦞 MoltBot AI Assistant running on Azure Container Apps
- 💬 Discord Integration - Chat with your AI via Discord DMs
- 🔐 Secure by Default - Gateway token authentication + DM allowlist
- 📊 Azure Monitoring - Full observability via Log Analytics
┌─────────────────────────────────────────────────────────────────────────────┐
│ Azure Resource Group │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Azure Container Apps Environment ││
│ │ ││
│ │ ┌───────────────────────────────────────────────────────────────────┐ ││
│ │ │ MoltBot Container App │ ││
│ │ │ │ ││
│ │ │ • Gateway (port 18789) • Discord Bot Connection │ ││
│ │ │ • Control UI (web chat) • OpenRouter API Integration │ ││
│ │ │ • Dynamic Config Generation • DM Allowlist Security │ ││
│ │ └───────────────────────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Container Registry │ │ Managed Identity │ │ Log Analytics │ │
│ │ │ │ │ │ │ │
│ │ Stores MoltBot │ │ Secure ACR access │ │ Logs & metrics │ │
│ │ container image │ │ (no passwords!) │ │ for monitoring │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
- ✅ Azure subscription with Contributor access
- ✅ Azure Developer CLI (azd) installed
- ✅ Azure CLI installed
- ✅ OpenRouter API key for LLM access
- ✅ Discord account for bot creation
The fastest way to deploy MoltBot is using Azure Developer CLI (azd). This provisions all infrastructure, builds the container image, and deploys everything in one command.
Before deploying, you need a Discord bot token:
- Go to Discord Developer Portal
- Click New Application → Name it (e.g., "MoltBot-Azure")
- Go to Bot → Click Add Bot (or Reset Token if exists)
- Enable these Privileged Gateway Intents:
- ✅ Message Content Intent
- ✅ Server Members Intent
- Click Reset Token → Copy the bot token (save it!)
- Go to OAuth2 → URL Generator:
- Scopes:
bot,applications.commands - Bot Permissions:
Send Messages,Read Message History,View Channels
- Scopes:
- Copy the generated OAuth2 URL - you'll need this to invite the bot later
Get Your Discord User ID:
- In Discord: Settings → Advanced → Enable Developer Mode
- Right-click your username → Copy User ID
# Clone this sample
git clone https://github.com/BandaruDheeraj/moltbot-azure-container-apps.git
cd moltbot-azure-container-apps
# Login to Azure
azd auth login
# Provision infrastructure (creates ACR, Container Apps Environment, etc.)
azd provisionWhen prompted, enter:
- Environment name: e.g.,
MoltBot-prod - Azure subscription: Select your subscription
- Location: e.g.,
eastus2
# Get your ACR name
ACR_NAME=$(az acr list --resource-group rg-MoltBot-prod --query "[0].name" -o tsv)
# Build the image in Azure (no local Docker needed!)
az acr build --registry $ACR_NAME --image "moltbot:latest" --file src/MoltBot/Dockerfile src/MoltBot/Understanding this command:
--registry $ACR_NAME- Build in your ACR (in the cloud)--image "moltbot:latest"- Name the output image (we choose this name)--file src/MoltBot/Dockerfile- Use the Dockerfile from this reposrc/MoltBot/- Send this folder as build context
This takes about 3-5 minutes. The Dockerfile automatically:
- Clones the official MoltBot source from GitHub
- Installs dependencies and builds the app
- Adds our custom
entrypoint.shfor Azure configuration
Note: You don't need to download MoltBot separately - it's pulled fresh during the build. The resulting image is stored in your ACR as
moltbot:latest.
# Set your required secrets
azd env set OPENROUTER_API_KEY "sk-or-v1-your-key-here"
azd env set DISCORD_BOT_TOKEN "your-discord-bot-token"
azd env set DISCORD_ALLOWED_USERS "your-discord-user-id"Where to get these values:
| Variable | Where to Get It |
|---|---|
OPENROUTER_API_KEY |
openrouter.ai/keys |
DISCORD_BOT_TOKEN |
Discord Developer Portal → Your App → Bot → Reset Token |
DISCORD_ALLOWED_USERS |
Discord → Settings → Advanced → Developer Mode → Right-click username → Copy User ID |
Optional settings:
azd env set MOLTBOT_MODEL "openrouter/anthropic/claude-3.5-sonnet"
azd env set MOLTBOT_PERSONA_NAME "Clawd"
azd env set ALLOWED_IP_RANGES "1.2.3.4/32" # IP restrictions
azd env set ALERT_EMAIL_ADDRESS "your-email@example.com"azd deploy- Open the OAuth2 URL from Step 1 to invite the bot to a server
- Find the bot in the server's member list
- Right-click → Message to start a DM
- Send:
Hello! - Wait a few seconds for the response 🎉
| Resource | Purpose |
|---|---|
| Azure Container Registry | Stores your MoltBot container image |
| Container Apps Environment | Hosting platform with built-in scaling |
| MoltBot Container App | Your AI assistant (1 CPU, 2GB RAM) |
| Managed Identity | Secure passwordless access to ACR |
| Log Analytics Workspace | Logs and monitoring |
| Storage Account | Persistent data storage |
# Change configuration (e.g., add another Discord user)
azd env set DISCORD_ALLOWED_USERS "user1-id,user2-id"
azd deploy
# Rebuild image with latest MoltBot
az acr build --registry $ACR_NAME --image "moltbot:latest" --file src/MoltBot/Dockerfile src/MoltBot/
azd deployIf you prefer to deploy step-by-step without azd, follow these instructions:
# Variables - customize these
RESOURCE_GROUP="rg-MoltBot"
LOCATION="eastus2"
ENVIRONMENT_NAME="cae-MoltBot"
ACR_NAME="crMoltBot$(openssl rand -hex 4)" # Must be globally unique
IDENTITY_NAME="MoltBot-identity"
APP_NAME="MoltBot"
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
# Create Azure Container Registry
az acr create --resource-group $RESOURCE_GROUP --name $ACR_NAME --sku Basic
# Create User-Assigned Managed Identity
az identity create --resource-group $RESOURCE_GROUP --name $IDENTITY_NAME
# Get identity details
IDENTITY_ID=$(az identity show --resource-group $RESOURCE_GROUP --name $IDENTITY_NAME --query id -o tsv)
IDENTITY_CLIENT_ID=$(az identity show --resource-group $RESOURCE_GROUP --name $IDENTITY_NAME --query clientId -o tsv)
IDENTITY_PRINCIPAL_ID=$(az identity show --resource-group $RESOURCE_GROUP --name $IDENTITY_NAME --query principalId -o tsv)
# Grant identity access to ACR
ACR_ID=$(az acr show --resource-group $RESOURCE_GROUP --name $ACR_NAME --query id -o tsv)
az role assignment create --assignee $IDENTITY_PRINCIPAL_ID --role AcrPull --scope $ACR_ID
# Create Container Apps Environment
az containerapp env create \
--name $ENVIRONMENT_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATIONMoltBot must be built from source. We use Azure Container Registry Tasks (no local Docker required):
# Build the image in ACR (runs in the cloud)
az acr build \
--registry $ACR_NAME \
--image "moltbot:v1" \
--file src/MoltBot/Dockerfile \
src/MoltBot/This takes about 5 minutes. The build:
- Clones MoltBot from GitHub
- Installs dependencies with pnpm
- Builds the TypeScript application
- Builds the Control UI
- Copies our custom
entrypoint.shfor Azure configuration
- Go to Discord Developer Portal
- Click New Application → Name it (e.g., "MoltBot-Azure")
- Go to Bot → Click Add Bot
- Enable these Privileged Gateway Intents:
- ✅ Message Content Intent
- ✅ Server Members Intent
- Click Reset Token → Copy the bot token (save it securely!)
- Go to OAuth2 → URL Generator:
- Scopes:
bot,applications.commands - Bot Permissions:
Send Messages,Read Message History,View Channels
- Scopes:
- Copy the generated URL and open it to invite the bot to your server
Get Your Discord User ID:
- In Discord: Settings → Advanced → Enable Developer Mode
- Right-click your username → Copy User ID
# Generate a secure random token for gateway authentication
GATEWAY_TOKEN=$(openssl rand -hex 16)
echo "Gateway Token: $GATEWAY_TOKEN"
# Save this! You'll need it for the Control UI# Set your actual values here
OPENROUTER_API_KEY="sk-or-v1-your-key-here"
DISCORD_BOT_TOKEN="your-discord-bot-token"
DISCORD_USER_ID="your-discord-user-id"
# Create the Container App with secrets
az containerapp create \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--environment $ENVIRONMENT_NAME \
--image "${ACR_NAME}.azurecr.io/moltbot:v1" \
--registry-server "${ACR_NAME}.azurecr.io" \
--registry-identity $IDENTITY_ID \
--user-assigned $IDENTITY_ID \
--target-port 18789 \
--ingress external \
--min-replicas 1 \
--max-replicas 1 \
--cpu 1.0 \
--memory 2Gi \
--secrets \
"openrouter-api-key=$OPENROUTER_API_KEY" \
"discord-bot-token=$DISCORD_BOT_TOKEN" \
"gateway-token=$GATEWAY_TOKEN" \
--env-vars \
"OPENROUTER_API_KEY=secretref:openrouter-api-key" \
"DISCORD_BOT_TOKEN=secretref:discord-bot-token" \
"MOLTBOT_GATEWAY_TOKEN=secretref:gateway-token" \
"DISCORD_ALLOWED_USERS=$DISCORD_USER_ID" \
"MOLTBOT_MODEL=openrouter/anthropic/claude-3.5-sonnet" \
"MOLTBOT_PERSONA_NAME=Clawd" \
"GATEWAY_PORT=18789" \
"NODE_ENV=production"# Get the Container App URL
az containerapp show --name $APP_NAME --resource-group $RESOURCE_GROUP --query "properties.configuration.ingress.fqdn" -o tsvImportant: Discord requires you to share a server with the bot before you can DM it.
- Create or use an existing Discord server where you can add the bot
- Invite the bot using the OAuth2 URL you generated earlier:
https://discord.com/oauth2/authorize?client_id=<BOT_USER_ID>&permissions=274877991936&scope=bot%20applications.commands - Find the bot in the server's member list (right sidebar)
- Right-click the bot → Message to open a DM
- Send:
Hello! - Wait a few seconds for the response
The Control UI is available but shows "pairing required" by default. Discord DMs are the primary interface.
To access the Control UI:
https://<your-app-url>/?token=<your-gateway-token>
| Variable | Required | Description |
|---|---|---|
OPENROUTER_API_KEY |
✅ | OpenRouter API key for LLM access |
DISCORD_BOT_TOKEN |
✅ | Discord bot token from Developer Portal |
MOLTBOT_GATEWAY_TOKEN |
✅ | Random token for gateway authentication |
DISCORD_ALLOWED_USERS |
✅ | Your Discord user ID (DM allowlist) |
MOLTBOT_MODEL |
No | Model ID (default: openrouter/anthropic/claude-3.5-sonnet) |
MOLTBOT_PERSONA_NAME |
No | Bot name (default: Clawd) |
| Parameter | Default | Description |
|---|---|---|
ALLOWED_IP_RANGES |
(empty) | Comma-separated CIDR blocks allowed to access the gateway (e.g., 1.2.3.4/32,10.0.0.0/8) |
INTERNAL_ONLY |
false |
Deploy with no public ingress (VNet-only access) |
ENABLE_ALERTS |
true |
Deploy Azure Monitor alerts for security events |
ALERT_EMAIL_ADDRESS |
(empty) | Email for alert notifications |
Enable IP restrictions:
azd env set ALLOWED_IP_RANGES "1.2.3.4/32"
azd deployEnable email alerts:
azd env set ALERT_EMAIL_ADDRESS "security@example.com"
azd deploy| Model | ID |
|---|---|
| Claude 3.5 Sonnet | openrouter/anthropic/claude-3.5-sonnet |
| Claude 3 Opus | openrouter/anthropic/claude-3-opus |
| GPT-4 Turbo | openrouter/openai/gpt-4-turbo |
| Gemini Pro | openrouter/google/gemini-pro |
- ✅
openrouter/anthropic/claude-3.5-sonnet - ❌
openrouter/anthropic/claude-sonnet-4-5(doesn't exist)
See OpenRouter Models for the full list.
The entrypoint.sh script dynamically generates MoltBot's configuration from environment variables at container startup:
{
"agents": {
"defaults": {
"model": { "primary": "openrouter/anthropic/claude-3.5-sonnet" }
},
"list": [{ "id": "main", "identity": { "name": "Clawd" } }]
},
"channels": {
"discord": {
"enabled": true,
"dm": { "policy": "allowlist", "allowFrom": ["your-user-id"] }
}
},
"gateway": {
"auth": { "mode": "token", "token": "<your-gateway-token>" }
}
}This approach:
- Keeps secrets out of the container image
- Allows configuration changes without rebuilding
- Generates proper MoltBot JSON config format
# Change model
az containerapp update --name $APP_NAME --resource-group $RESOURCE_GROUP \
--set-env-vars "MOLTBOT_MODEL=openrouter/anthropic/claude-3-opus"
# Add another allowed Discord user
az containerapp update --name $APP_NAME --resource-group $RESOURCE_GROUP \
--set-env-vars "DISCORD_ALLOWED_USERS=user1-id,user2-id"# Update API key
az containerapp secret set --name $APP_NAME --resource-group $RESOURCE_GROUP \
--secrets "openrouter-api-key=sk-or-v1-new-key"
# Restart to apply secret changes
REVISION=$(az containerapp show --name $APP_NAME --resource-group $RESOURCE_GROUP --query "properties.latestRevisionName" -o tsv)
az containerapp revision restart --name $APP_NAME --resource-group $RESOURCE_GROUP --revision $REVISION# Rebuild with latest MoltBot
az acr build --registry $ACR_NAME --image "moltbot:v2" \
--file src/MoltBot/Dockerfile src/MoltBot/
# Deploy new image
az containerapp update --name $APP_NAME --resource-group $RESOURCE_GROUP \
--image "${ACR_NAME}.azurecr.io/moltbot:v2"# Stream live logs
az containerapp logs show --name $APP_NAME --resource-group $RESOURCE_GROUP \
--follow --tail 50 --type console
# Check for errors
az containerapp logs show --name $APP_NAME --resource-group $RESOURCE_GROUP \
--tail 100 --type console | grep -i error✅ Healthy startup:
Discord channel configured: yes (DM allowlist: 123456789)
MoltBot configuration written to /home/node/.MoltBot/MoltBot.json
Gateway token configured: yes
[discord] logged in to discord as 987654321
[gateway] agent model: openrouter/anthropic/claude-3.5-sonnet
[gateway] listening on ws://0.0.0.0:18789
❌ Common errors:
Unknown model: ...- Check the model ID format (must be exact)HTTP 401: authentication_error- Invalid API key[discord] channel exited- Invalid Discord bot token
-
Check logs for errors:
az containerapp logs show --name $APP_NAME --resource-group $RESOURCE_GROUP --tail 50
-
Verify Discord connection: Look for:
[discord] logged in to discord as <bot-id> -
Check DM allowlist: Make sure your Discord user ID is in
DISCORD_ALLOWED_USERS -
Verify model format: Must be exactly
openrouter/anthropic/claude-3.5-sonnet(not variations likeclaude-sonnet-4-5)
The model ID format is very specific. Common mistakes:
- ❌
anthropic/claude-sonnet-4-5→ Model doesn't exist - ❌
openrouter:anthropic/claude-3.5-sonnet→ Wrong prefix format - ✅
openrouter/anthropic/claude-3.5-sonnet→ Correct!
- Verify your OpenRouter API key at openrouter.ai/keys
- Check the key has credits available
- Update the secret:
az containerapp secret set --name $APP_NAME --resource-group $RESOURCE_GROUP \ --secrets "openrouter-api-key=sk-or-v1-correct-key"
- Restart the container to apply
-
Check if image exists:
az acr repository show-tags --name $ACR_NAME --repository MoltBot -
Verify managed identity has ACR pull permission:
az role assignment list --assignee $IDENTITY_PRINCIPAL_ID --scope $ACR_ID
Discord requires bots and users to share at least one server:
- Create a private Discord server (just for you and the bot)
- Invite the bot using the OAuth2 URL
- Now you can DM the bot
This deployment addresses common security concerns raised by the community:
| Concern | How ACA Addresses It |
|---|---|
| 1. Close ports / IP allowlist | ✅ Built-in ingress IP restrictions via ALLOWED_IP_RANGES |
| 2. Auth (strong secret + TLS) | ✅ Gateway token auth + automatic HTTPS certificates |
| 3. Rotate keys | ✅ az containerapp secret set + restart |
| 4. Rate limit + logs + alerts | ✅ Log Analytics + 4 preconfigured Azure Monitor alerts |
The deployment includes four Azure Monitor alerts (enabled by default):
| Alert | Trigger | Indicates |
|---|---|---|
| High Error Rate | >10 auth errors in 5 min | Brute force attack |
| Container Restarts | >3 restarts in 15 min | Crash or OOM attack |
| Unusual Activity | >100 messages/hour | Abuse |
| Channel Disconnect | Discord goes offline | Token issue |
Restrict who can access your MoltBot gateway:
# Only allow specific IPs (e.g., your home + VPN)
azd env set ALLOWED_IP_RANGES "1.2.3.4/32,10.0.0.0/8"
azd deployFor maximum security, deploy with no public ingress:
azd env set INTERNAL_ONLY "true"
azd deployThis makes MoltBot accessible only from within your Azure VNet.
Rotate API keys without rebuilding:
# Rotate OpenRouter API key
az containerapp secret set --name MoltBot --resource-group $RESOURCE_GROUP \
--secrets "openrouter-api-key=sk-or-v1-new-key"
# Restart to apply
REVISION=$(az containerapp show --name MoltBot --resource-group $RESOURCE_GROUP \
--query "properties.latestRevisionName" -o tsv)
az containerapp revision restart --name MoltBot --resource-group $RESOURCE_GROUP \
--revision $REVISION| Feature | Azure Container Apps | VPS (Hetzner/DO) | Home Server |
|---|---|---|---|
| IP Restrictions | ✅ Built-in | ||
| Automatic TLS | ✅ Free certs | ❌ Manual | ❌ Manual |
| Secrets Management | ✅ Native | ❌ .env files | ❌ .env files |
| Security Alerts | ✅ Azure Monitor | ❌ Self-built | ❌ None |
| Container Isolation | ✅ Hyper-V | ❌ None | |
| Compliance | ✅ SOC2/ISO/HIPAA | ❌ None | ❌ None |
| Resource | Monthly Cost |
|---|---|
| Container Apps (1 CPU, 2GB RAM, always-on) | ~$30-50 |
| Container Registry (Basic) | ~$5 |
| Log Analytics (1GB ingestion) | ~$2-5 |
| Total | ~$40-60/month |
Cost Optimization:
- Scale to 0 replicas when not in use (note: breaks Discord connection)
- Use a smaller/cheaper model via OpenRouter
- Monitor usage in Azure Portal
# Delete everything
az group delete --name $RESOURCE_GROUP --yes --no-waitDuring the development of this sample, we discovered several important details:
-
MoltBot requires config file, not just env vars - The gateway reads from
~/.MoltBot/MoltBot.json, so we need an entrypoint script to generate it from environment variables. -
Config schema matters - Use
agents.defaultsandagents.list[].identity, not the legacyagentandidentityformat. -
Model IDs must be exact -
claude-3.5-sonnetexists, butclaude-sonnet-4-5does not. Check OpenRouter for current model names. -
Discord requires shared server - You can't DM a Discord bot unless you share at least one server with it.
-
Secrets need restart - After updating Container App secrets, you must restart the revision for changes to take effect.
- MoltBot Documentation
- MoltBot Discord Channel Setup
- MoltBot Model Providers
- OpenRouter API
- Azure Container Apps Documentation
🦞 Built with MoltBot. Questions? Check docs.molt.bot