Skip to Content

Odoo on Azure: how to host it properly

Odoo on Azure: how to host it properly
November 4, 2026 · 25 min read

Short answer

To host Odoo on Azure, run it on a general purpose x86 VM with a Premium SSD disk. Put Postgres on the same VM or on Azure Database for PostgreSQL. Open only ports 80 and 443, and log in through Azure Bastion instead of a public SSH port. Send backups of the database and the files to a Blob Storage container that the VM can write to but not read or delete. Skip B-series VMs for live use, and test a restore before you go live.

Many companies already run on Microsoft 365 and Azure, so Odoo goes there too. Azure has a region close to almost anyone. It also has many ways to get Odoo slightly wrong. This guide gives you the exact commands for each Azure step.

For the install itself (Odoo, Postgres, the config file and the proxy), follow our guide on running Odoo on a VPS. This page covers what's different on Azure.

You can run every az command below from your own computer with the Azure CLI, or from Azure Cloud Shell in the portal. One step, the Bastion login, needs the CLI on your own computer. Replace the names in capitals with your own. The examples use the westeurope region. Use your own.

Which VM size to pick

Two things matter most for Odoo:

  • Memory. The minimum to start is 2 vCPU, 4 GB of memory and a 50 GB disk. That runs Odoo and Postgres on one VM for a small team. Each worker needs its share of memory, and Postgres wants its own. For more users, big imports or Azure Database for PostgreSQL, our guide on how many Odoo workers you need shows how to size it.
  • Steady CPU. Odoo is busy all day while people work in it.

Why not a B-series VM

B-series VMs are "burstable." They earn CPU credits while they are quiet and spend them when they are busy. When the credits run out, Azure slows the VM down to its base level until it earns more. Odoo used all day can run out of credits in the middle of the work day. Then every page gets slow.

So for production, pick a general purpose size with fixed CPU, for example:

  • Standard_D2s_v5 (Intel) or Standard_D2as_v5 (AMD). 2 vCPU and 8 GiB of memory. A good default.
  • A newer generation, such as Standard_D2s_v6, if your region has it.
  • An E-series size (more memory per CPU) when Postgres runs on the same VM and the database is large.

B-series VMs are fine for a test copy that few people use.

Pick an x86 size

Azure also sells Arm sizes, such as Dpsv5 and Dpsv6 (Azure Cobalt). Pick an Intel or AMD size instead. If you may hand the Odoo work to us later, this matters: our bring your own server software runs on x86 servers only.

Image

Use Ubuntu Server 22.04 LTS or Debian 12. wkhtmltopdf, which Odoo uses to print PDFs, has official packages for both (jammy and bookworm). Print an invoice before you go live.

In the CLI, Ubuntu 22.04 is the image Ubuntu2204. For Debian 12, find the exact image name with:

az vm image list --publisher Debian --offer debian-12 --all --output table

Disk: Premium SSD

Use a Premium SSD (Premium_LRS) for the OS disk. Premium SSD v2 is faster, but it can't be the OS disk. You can add one later as a data disk if you need it.

On Premium SSD, speed grows with disk size. A 128 GiB disk gets more IOPS than a 64 GiB one. Leave room for the filestore and three days of local backups.

Region

Pick the region with care. Your VM, database and disks live in the region you choose. Put the VM, the database and the backup storage in the same region, unless you want backups in a second region on purpose.

Network and security rules

Create the network

Everything goes in one resource group and one virtual network (VNet). The VNet gets three subnets: one for the VM, one for Azure Bastion, and one for the database.

az group create --name odoo-rg --location westeurope

az network vnet create --resource-group odoo-rg --name odoo-vnet \
  --address-prefixes 10.0.0.0/16 \
  --subnet-name odoo-web --subnet-prefixes 10.0.1.0/24

az network vnet subnet create --resource-group odoo-rg --vnet-name odoo-vnet \
  --name AzureBastionSubnet --address-prefixes 10.0.2.0/26

az network vnet subnet create --resource-group odoo-rg --vnet-name odoo-vnet \
  --name odoo-db --address-prefixes 10.0.3.0/27 \
  --delegations Microsoft.DBforPostgreSQL/flexibleServers

Bastion needs a subnet with the exact name AzureBastionSubnet, size /26 or larger. The database subnet is "delegated." Only Azure Database for PostgreSQL can use it. You can't make it bigger later, so don't go below /27. Skip the odoo-db subnet if Postgres stays on the VM.

Network security group

Create a network security group (NSG) and allow only web traffic from the internet:

az network nsg create --resource-group odoo-rg --name odoo-web-nsg

az network nsg rule create --resource-group odoo-rg --nsg-name odoo-web-nsg \
  --name allow-web --priority 100 --direction Inbound --access Allow \
  --protocol Tcp --source-address-prefixes Internet \
  --destination-port-ranges 80 443

az network nsg rule create --resource-group odoo-rg --nsg-name odoo-web-nsg \
  --name allow-ssh-from-bastion --priority 110 --direction Inbound --access Allow \
  --protocol Tcp --source-address-prefixes 10.0.2.0/26 \
  --destination-port-ranges 22

az network vnet subnet update --resource-group odoo-rg --vnet-name odoo-vnet \
  --name odoo-web --network-security-group odoo-web-nsg

Port 22 is open only to the Bastion subnet, not to the internet. Azure's default rules block all other traffic from the internet. Odoo's own ports (8069 and 8072) stay closed. Only nginx on the VM talks to them.

Check the rules:

az network nsg rule list --resource-group odoo-rg --nsg-name odoo-web-nsg --output table

Create the VM

A fixed address

Create a Standard public IP first. Standard IPs are always static, so the address doesn't change when the VM stops and starts:

az network public-ip create --resource-group odoo-rg --name odoo-ip --sku Standard
az network public-ip show --resource-group odoo-rg --name odoo-ip \
  --query "{ip:ipAddress, method:publicIPAllocationMethod}" --output table

The method should say Static. Point your domain's DNS record at that address before you ask Let's Encrypt for a certificate.

Launch it

az vm create \
  --resource-group odoo-rg --name odoo-vm \
  --image Ubuntu2204 \
  --size Standard_D2s_v5 \
  --vnet-name odoo-vnet --subnet odoo-web \
  --nsg "" \
  --public-ip-address odoo-ip \
  --storage-sku Premium_LRS --os-disk-size-gb 128 \
  --assign-identity \
  --admin-username azureuser --generate-ssh-keys

What these do:

  • --nsg "" adds no second NSG on the VM itself. The subnet's NSG from above applies.
  • --assign-identity gives the VM a system-assigned managed identity. It's the VM's own identity in Azure. It lets the VM write backups and send disk numbers without any password or key stored on it.
  • --generate-ssh-keys keeps a spare key for the azureuser account. You won't use it day to day.

Check that the identity exists, and note its ID. You need it later:

az vm identity show --resource-group odoo-rg --name odoo-vm --query principalId --output tsv

If a VM already runs without an identity, add one with:

az vm identity assign --resource-group odoo-rg --name odoo-vm

Check that Odoo's ports are closed

Once Odoo runs, check from your own computer that only nginx answers:

nc -zv -w 5 YOUR_PUBLIC_IP 8069

This should time out. If it connects, check the NSG rules above.

Log in with Bastion, not a public SSH port

Azure Bastion gives you a shell on the VM without opening port 22 to the internet. You sign in with your Microsoft Entra ID account, so there's no SSH key to share or lose.

Turn on Entra ID login on the VM

Install the login extension, and give yourself the admin login role:

az vm extension set --resource-group odoo-rg --vm-name odoo-vm \
  --publisher Microsoft.Azure.ActiveDirectory --name AADSSHLoginForLinux

az role assignment create \
  --role "Virtual Machine Administrator Login" \
  --assignee "$(az account show --query user.name --output tsv)" \
  --scope "$(az group show --name odoo-rg --query id --output tsv)"

The extension needs the VM's managed identity, which you already added. Owning the subscription is not enough to log in. You need this role.

Create Bastion

Connecting from your own terminal needs the Standard SKU, with "tunneling" (native client support) turned on:

az network public-ip create --resource-group odoo-rg --name odoo-bastion-ip --sku Standard

az network bastion create --resource-group odoo-rg --name odoo-bastion \
  --vnet-name odoo-vnet --public-ip-address odoo-bastion-ip \
  --sku Standard --enable-tunneling true --location westeurope

It takes a few minutes. Bastion is billed for every hour it exists.

Connect

From your own computer (this command doesn't work in Cloud Shell):

az extension add --name ssh
az login
VM_ID=$(az vm show --resource-group odoo-rg --name odoo-vm --query id --output tsv)
az network bastion ssh --name odoo-bastion --resource-group odoo-rg \
  --target-resource-id "$VM_ID" --auth-type AAD

You land as your Entra ID user, and you can use sudo.

Where Postgres goes

You have two good options:

  • On the same VM. Simple, and fine for many teams. Follow the VPS guide as written.
  • On Azure Database for PostgreSQL (Flexible Server). Azure handles the database server, patches and point-in-time restore. The steps are below.

Set up the private DNS zone

With private access, the database has no public address. VMs in the VNet find it through a private DNS zone. Create the zone and link it to the VNet:

az network private-dns zone create --resource-group odoo-rg \
  --name odoo.private.postgres.database.azure.com

az network private-dns link vnet create --resource-group odoo-rg \
  --zone-name odoo.private.postgres.database.azure.com \
  --name odoo-vnet-link --virtual-network odoo-vnet --registration-enabled false

The zone name must end with postgres.database.azure.com.

Create the database server

Type the admin password into a variable, so it stays out of your shell history:

read -s -p "Admin password: " PGADMIN_PW; echo

az postgres flexible-server create \
  --resource-group odoo-rg --name YOUR_DB_SERVER --location westeurope \
  --tier GeneralPurpose --sku-name Standard_D2ds_v5 \
  --version 17 \
  --storage-size 64 --storage-auto-grow Enabled \
  --admin-user odoo_admin --admin-password "$PGADMIN_PW" \
  --vnet odoo-vnet --subnet odoo-db \
  --private-dns-zone odoo.private.postgres.database.azure.com \
  --backup-retention 14

What these do:

  • --tier GeneralPurpose avoids the Burstable tier, which has the same credit limits as B-series VMs.
  • --version 17 is an example. Pick a version your Odoo release supports.
  • --storage-auto-grow Enabled grows the disk before it fills. A full server switches to read-only.
  • --backup-retention 14 keeps 14 days of server backups. The default is 7, and the maximum is 35.
  • Add --geo-redundant-backup Enabled to also copy backups to the paired region. You can only choose this when you create the server.
  • Add --zonal-resiliency Enabled for a standby copy in another zone (high availability). Older CLI versions call this --high-availability ZoneRedundant.

From the VM, check that the name points to a private address in 10.0.3.0/27:

getent hosts YOUR_DB_SERVER.postgres.database.azure.com

Allow the pg_trgm extension

Odoo turns on the pg_trgm extension when it creates a database. Azure only allows extensions you list first:

az postgres flexible-server parameter set --resource-group odoo-rg \
  --server-name YOUR_DB_SERVER --name azure.extensions --value pg_trgm

If you set unaccent = True in odoo.conf, use --value pg_trgm,unaccent. Check the list with SHOW azure.extensions; in psql.

Create the odoo database user

Odoo needs its own user that can create databases. Don't use the admin user for daily work. The admin user is not a superuser. It's a member of azure_pg_admin, with the rights to create roles and databases.

On the VM, install the Postgres client (see "Match the Postgres client version" below), then connect:

psql "host=YOUR_DB_SERVER.postgres.database.azure.com user=odoo_admin dbname=postgres sslmode=require"
CREATE ROLE odoo WITH LOGIN CREATEDB PASSWORD 'a-long-random-password';

Odoo's live chat connects to the postgres database, not only to yours. Check that the odoo user can log in there:

psql "host=YOUR_DB_SERVER.postgres.database.azure.com user=odoo dbname=postgres sslmode=require" -c 'SELECT current_user;'

Let Odoo create yourdb itself. On its first start with -d yourdb, Odoo creates the database as the odoo user, so odoo owns it. Check the owner with \l yourdb in psql.

Don't create it as the admin with CREATE DATABASE yourdb OWNER odoo. On Postgres 16 and later, that fails unless the admin is allowed to act as odoo.

Point Odoo at the database server

In odoo.conf:

db_host = YOUR_DB_SERVER.postgres.database.azure.com
db_port = 5432
db_user = odoo
db_password = a-long-random-password
db_sslmode = require

Azure turns on require_secure_transport by default. It refuses connections without TLS. db_sslmode = require makes Odoo always use it.

Connection limits and PgBouncer

Each Odoo process can open up to db_maxconn connections (64 by default). Add up your workers, cron threads and the chat process, and multiply by db_maxconn. Keep that under the server's limit. A Standard_D2ds_v5 server allows 844 user connections. A small Burstable B1ms allows only 35, so it needs a much lower db_maxconn.

Azure has a built-in PgBouncer on port 6432. It pools in transaction mode by default. Odoo's live chat holds a LISTEN connection, and transaction pooling breaks it. Keep Odoo on port 5432.

Match the Postgres client version

pg_dump will not dump a server with a newer major version than itself. If the server runs Postgres 17, the VM needs the version 17 client. Install it from the PostgreSQL apt repository:

sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install -y postgresql-client-17

Server backups don't cover the filestore

Azure backs up the database server and can restore it to any point in the retention window. It does not back up the filestore. Attachments and images live on the VM disk, under /var/lib/odoo/filestore. You still need the Blob Storage backup below, so the database and the files are saved together.

Backups to Blob Storage

Create the storage account

Storage account names are shared by all Azure customers. Use 3 to 24 lowercase letters and numbers, and make it unique:

az storage account create --resource-group odoo-rg --name STORAGE_ACCOUNT \
  --location westeurope --kind StorageV2 --sku Standard_GRS \
  --min-tls-version TLS1_2 \
  --allow-blob-public-access false \
  --allow-shared-key-access false

What these do:

  • Standard_GRS keeps a second copy in the paired region. Check that the pair fits your data rules. If it doesn't, use Standard_ZRS (copies in three zones of one region).
  • --allow-blob-public-access false means no container can ever be made public.
  • --allow-shared-key-access false turns off the account keys. Every request must come from an Entra ID identity, like the VM's. A stolen key can't exist.

Check it:

az storage account show --resource-group odoo-rg --name STORAGE_ACCOUNT \
  --query "{publicAccess:allowBlobPublicAccess, sharedKey:allowSharedKeyAccess}"

Both should be false.

Turn on versioning and soft delete

Versioning keeps the old copy when a file is overwritten or deleted. Soft delete keeps deleted files and versions for a set number of days:

az storage account blob-service-properties update --resource-group odoo-rg \
  --account-name STORAGE_ACCOUNT \
  --enable-versioning true \
  --enable-delete-retention true --delete-retention-days 14 \
  --enable-container-delete-retention true --container-delete-retention-days 14

Create the container

az storage container-rm create --resource-group odoo-rg \
  --storage-account STORAGE_ACCOUNT --name your-odoo-backups --enable-vlw

--enable-vlw doesn't lock anything yet. It only makes the optional lock below possible later.

Remove old backups on a schedule

Save this as lifecycle.json:

{
  "rules": [
    {
      "enabled": true,
      "name": "expire-old-odoo-backups",
      "type": "Lifecycle",
      "definition": {
        "filters": {
          "blobTypes": ["blockBlob"]
        },
        "actions": {
          "baseBlob": {
            "delete": { "daysAfterModificationGreaterThan": 30 }
          },
          "version": {
            "delete": { "daysAfterCreationGreaterThan": 30 }
          }
        }
      }
    }
  ]
}
az storage account management-policy create --resource-group odoo-rg \
  --account-name STORAGE_ACCOUNT --policy @lifecycle.json

Backups older than 30 days are removed. With versioning on, "removed" first means "kept as an older version." The older version stays 30 more days, then goes. Soft delete then holds it 14 more days. A new policy can take up to 24 hours to start. This command replaces any lifecycle policy the account had before.

Let the VM write, and only write

Azure has no built-in role that can only write blobs. So you make one. It allows two actions: write a blob, and add a new one. No read, no list, no delete. Save this as odoo-backup-writer.json, with your subscription ID:

{
  "Name": "Odoo Backup Writer",
  "IsCustom": true,
  "Description": "Upload Odoo backups. No read, list or delete.",
  "Actions": [],
  "NotActions": [],
  "DataActions": [
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write",
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action"
  ],
  "NotDataActions": [],
  "AssignableScopes": [
    "/subscriptions/SUBSCRIPTION_ID/resourceGroups/odoo-rg"
  ]
}

az account show --query id --output tsv prints your subscription ID. Create the role, and give it to the VM's identity on the backup container only:

az role definition create --role-definition odoo-backup-writer.json

PRINCIPAL_ID=$(az vm identity show --resource-group odoo-rg --name odoo-vm \
  --query principalId --output tsv)
CONTAINER_SCOPE="$(az storage account show --resource-group odoo-rg \
  --name STORAGE_ACCOUNT --query id --output tsv)/blobServices/default/containers/your-odoo-backups"

az role assignment create --assignee-object-id "$PRINCIPAL_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Odoo Backup Writer" --scope "$CONTAINER_SCOPE"

A new role and its assignment can take a few minutes to work.

The VM can upload, but it can't list, read or delete anything in the container. If someone breaks into the server, they can't read your old backups or wipe them. Writing over a file is allowed, but versioning keeps the old copy.

If your upload fails with a permission error (403), the upload tool needs more than these two actions. Then give the VM's identity the built-in Storage Blob Data Contributor role on the same container scope instead. That role can read and delete current files. It can't delete older versions, so versioning and soft delete still keep your copies.

Optional: lock backups for a set time

If your rules need more, a time-based retention policy blocks deletes for a set number of days, even by an admin:

az storage container immutability-policy create \
  --account-name STORAGE_ACCOUNT --container-name your-odoo-backups --period 14

The policy starts "unlocked," so you can still change or remove it while you test. Once you lock it in the portal (Container > Access policy > Lock policy), you can't remove it. Keep the period shorter than the 30 day lifecycle rule. Test it on a spare account first.

Install AzCopy on the VM

curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y azcopy

On Debian 12, use config/debian/12/ in the address. AzCopy uses the VM's managed identity when AZCOPY_AUTO_LOGIN_TYPE=MSI is set. You never store a key or password on the server.

The backup script

Create the local folder first:

sudo mkdir -p /backups
sudo chown odoo:odoo /backups

Save this as /usr/local/bin/odoo-backup.sh and run sudo chmod 755 on it:

#!/bin/bash
# /usr/local/bin/odoo-backup.sh, run as the odoo user
set -euo pipefail
DB=yourdb
DAY=$(date +%F)
DIR=/backups/$DAY
mkdir -p "$DIR"
pg_dump -Fc "$DB" > "$DIR/$DB.dump"
tar -czf "$DIR/$DB-filestore.tar.gz" -C /var/lib/odoo/filestore "$DB"
AZCOPY_AUTO_LOGIN_TYPE=MSI azcopy copy "$DIR/*" "https://STORAGE_ACCOUNT.blob.core.windows.net/your-odoo-backups/$DAY"
find /backups -mindepth 1 -maxdepth 1 -type d -mtime +3 -exec rm -rf {} +

With Azure Database for PostgreSQL, pg_dump must connect over the network. Change the pg_dump line to:

pg_dump -h YOUR_DB_SERVER.postgres.database.azure.com -U odoo -Fc "$DB" > "$DIR/$DB.dump"

Then give the odoo user its password in a .pgpass file, in its home folder:

sudo -u odoo -H bash -c 'echo "YOUR_DB_SERVER.postgres.database.azure.com:5432:*:odoo:a-long-random-password" > ~/.pgpass && chmod 600 ~/.pgpass'

The * lets the same line work for every database, including the restore test.

Run it every night

Create /etc/systemd/system/odoo-backup.service:

[Unit]
Description=Odoo backup to Blob Storage
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=odoo
ExecStart=/usr/local/bin/odoo-backup.sh

And /etc/systemd/system/odoo-backup.timer:

[Unit]
Description=Run the Odoo backup every night

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target

Turn it on, then run it once by hand and check the result:

sudo systemctl daemon-reload
sudo systemctl enable --now odoo-backup.timer
sudo systemctl start odoo-backup.service
journalctl -u odoo-backup.service -n 20

From your own computer, check that the files arrived. Your own account needs a data role to see them, even if you own the subscription. Give yourself read access to the container once:

az role assignment create --role "Storage Blob Data Reader" \
  --assignee "$(az account show --query user.name --output tsv)" \
  --scope "$CONTAINER_SCOPE"

az storage blob list --account-name STORAGE_ACCOUNT \
  --container-name your-odoo-backups --auth-mode login --output table

Test a restore

The production VM can't read the container. That's on purpose. So the restore test runs on a second VM with its own identity and read-only access. Create the VM the same way as above, with --name odoo-restore-vm and its own public IP (or none). Install Odoo on it with the VPS guide, and AzCopy as above. Then give its identity read access to the container:

RESTORE_ID=$(az vm identity show --resource-group odoo-rg --name odoo-restore-vm \
  --query principalId --output tsv)

az role assignment create --assignee-object-id "$RESTORE_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Reader" --scope "$CONTAINER_SCOPE"

On the test VM, as the odoo user, pull one night's backup and restore it:

mkdir -p /tmp/restore && cd /tmp/restore
AZCOPY_AUTO_LOGIN_TYPE=MSI azcopy copy "https://STORAGE_ACCOUNT.blob.core.windows.net/your-odoo-backups/2026-10-25/*" .
createdb -O odoo yourdb_restored
pg_restore -d yourdb_restored --no-owner --role=odoo yourdb.dump
mkdir -p /var/lib/odoo/filestore/yourdb_restored
tar -xzf yourdb-filestore.tar.gz -C /var/lib/odoo/filestore/yourdb_restored --strip-components=1

If the test copy uses Azure Database for PostgreSQL, add -h YOUR_DB_SERVER.postgres.database.azure.com -U odoo to createdb and pg_restore.

Before you start Odoo on the copy, neutralize it. A copy of production still has your mail servers and scheduled jobs, so it can email real customers. As the odoo user, run:

odoo neutralize -c /etc/odoo/odoo.conf -d yourdb_restored

Use odoo-bin instead of odoo if you run Odoo from source. This turns off outgoing mail servers, scheduled actions and payment providers in the copy. Our guide on copying production to staging explains what it covers and what it misses.

Then log in to yourdb_restored and open a few attachments. Our guide on how to know if an Odoo backup will restore covers the full test.

Get an alert before the disk fills up

Logs, the filestore and local backups all grow. Azure does not see how full your disk is unless the VM sends that number. The Azure Monitor Agent does this. A data collection rule (DCR) tells it what to send and where.

Create a Log Analytics workspace to hold the numbers, and install the agent:

az monitor log-analytics workspace create --resource-group odoo-rg \
  --workspace-name odoo-logs --location westeurope

WS_ID=$(az monitor log-analytics workspace show --resource-group odoo-rg \
  --workspace-name odoo-logs --query id --output tsv)
VM_ID=$(az vm show --resource-group odoo-rg --name odoo-vm --query id --output tsv)

az vm extension set --ids "$VM_ID" \
  --name AzureMonitorLinuxAgent --publisher Microsoft.Azure.Monitor \
  --enable-auto-upgrade true

The agent signs in with the VM's managed identity. Save this as odoo-disk-dcr.json. Put the value of $WS_ID in place of WORKSPACE_ID, and use your region:

{
  "location": "westeurope",
  "kind": "Linux",
  "properties": {
    "dataSources": {
      "performanceCounters": [
        {
          "name": "odooDisk",
          "streams": ["Microsoft-Perf"],
          "samplingFrequencyInSeconds": 60,
          "counterSpecifiers": ["\\Logical Disk(*)\\% Free Space"]
        }
      ]
    },
    "destinations": {
      "logAnalytics": [
        {
          "name": "odooLogs",
          "workspaceResourceId": "WORKSPACE_ID"
        }
      ]
    },
    "dataFlows": [
      {
        "streams": ["Microsoft-Perf"],
        "destinations": ["odooLogs"]
      }
    ]
  }
}

Create the rule and link it to the VM:

SUB_ID=$(az account show --query id --output tsv)
DCR_ID="/subscriptions/$SUB_ID/resourceGroups/odoo-rg/providers/Microsoft.Insights/dataCollectionRules/odoo-disk"

az rest --method put \
  --url "https://management.azure.com${DCR_ID}?api-version=2022-06-01" \
  --body @odoo-disk-dcr.json

az monitor data-collection rule association create \
  --name odoo-disk --resource "$VM_ID" --rule-id "$DCR_ID"

After about five minutes, check that numbers arrive. In the portal, open the VM, then Logs, and run:

Perf
| where ObjectName == "Logical Disk" and CounterName == "% Free Space"
| summarize arg_max(TimeGenerated, CounterValue) by InstanceName

You should see / with its free space in percent.

Create an action group that emails you, then the alert. It fires when free space on / drops below 20%, which means the disk is more than 80% full:

az monitor action-group create --resource-group odoo-rg \
  --name odoo-alerts --short-name odoo \
  --action email admin [email protected]

az monitor scheduled-query create --resource-group odoo-rg \
  --name odoo-disk-80 --scopes "$VM_ID" \
  --condition "min 'CounterValue' from 'DiskFree' < 20" \
  --condition-query DiskFree="Perf | where ObjectName == 'Logical Disk' and CounterName == '% Free Space' and InstanceName == '/'" \
  --evaluation-frequency 5m --window-size 15m \
  --action-groups odoo-alerts --severity 2

Know when Odoo is down

A disk alert won't tell you that the site stopped answering. An Application Insights standard test checks your login page from several places in the world.

Create an Application Insights resource that uses the same workspace:

az monitor app-insights component create --resource-group odoo-rg \
  --app odoo-uptime --location westeurope --workspace "$WS_ID"

Then in the portal:

  1. Open odoo-uptime, then Availability, then Add Standard test.
  2. Set the URL to https://your-domain/web/login. Keep retries on, and keep the SSL certificate check on.
  3. Pick at least five test locations, and save.
  4. In the test's menu, choose Open Rules (Alerts) page. Edit the rule and add the odoo-alerts action group. Without this, you only see alerts in the portal, with no email.

Outgoing email

Azure blocks outgoing mail on port 25 from VMs, unless the subscription is an Enterprise Agreement or a Microsoft Customer Agreement for enterprise. Odoo mail sent that way will time out. Send through an SMTP service on port 587 instead. Azure doesn't block port 587, whatever your subscription type. Your current mail provider works.

Azure Communication Services can also send email over SMTP:

  1. Create an Email Communication Services resource, add your domain and verify it with the DNS records it gives you. Connect it to a Communication Services resource.
  2. Register an app in Microsoft Entra ID and create a client secret for it.
  3. Give that app the Communication and Email Service Owner role on the Communication Services resource.
  4. In the Communication Services resource, open SMTP Usernames and add a username linked to the app.
  5. In Odoo, turn on developer mode. Go to Settings > Technical > Outgoing Mail Servers. Set the server to smtp.azurecomm.net, port 587, encryption TLS (STARTTLS). The username is the SMTP username from step 4. The password is the app's client secret.

Odoo traps on any server, Azure included

Websocket. Since Odoo 16, chat and live updates use port 8072. nginx must send /websocket there, with the upgrade headers.

Proxy mode. Odoo only trusts forwarded headers when proxy_mode = True and the request has X-Forwarded-Host. Without both, your links come out on http://.

Database manager. Set list_db = False and a strong admin_passwd.

Workers. Set workers above 0, sized to memory. Then Odoo starts the chat worker on gevent_port.

In odoo.conf:

proxy_mode = True
list_db = False
admin_passwd = a-long-random-string
workers = 4
gevent_port = 8072

In nginx, next to your location / block:

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

location /websocket {
  proxy_pass http://127.0.0.1:8072;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;
  proxy_set_header X-Forwarded-Host $http_host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Real-IP $remote_addr;
}

The map block goes in the http section, outside server.

Behind Application Gateway

You don't need a load balancer for one VM. If you use Application Gateway anyway:

  • Application Gateway handles websockets on its own. Keep nginx behind it, so /websocket still goes to 8072.
  • It sends X-Forwarded-Proto and X-Forwarded-For, and the original host in X-Original-Host. It doesn't send X-Forwarded-Host, and Odoo ignores the others without it. So nginx must add it.
  • It ends HTTPS, so nginx sees plain HTTP. If the gateway only listens on HTTPS, tell Odoo the request was HTTPS.

In both nginx location blocks, use:

proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;

$host works if the gateway keeps the original host name, which is the default. Then allow port 80 in odoo-web-nsg from the gateway's subnet only, not from the whole internet.

Before you go live

  • The VM is a general purpose x86 size (not B-series), with a Premium SSD disk
  • Odoo runs with workers above 0, sized to the server's memory
  • /websocket goes to port 8072, and chat works
  • proxy_mode = True, and links in emails start with https://
  • list_db = False and a strong admin_passwd
  • Only ports 80 and 443 are open to the internet, port 8069 times out, and you log in through Bastion
  • With Azure Database for PostgreSQL: private access, db_sslmode = require, and pg_trgm allowed
  • The SSL certificate renews on its own
  • PDFs print (wkhtmltopdf works)
  • Test emails arrive, sent on port 587
  • Logs rotate, the disk alert email works, and the uptime test is green
  • The backup timer ran, and the files are in Blob Storage
  • A backup has been restored on another VM, and the attachments open

Keep Azure, hand over the Odoo work

You can keep Odoo in your Azure subscription and still not run it yourself. With bring your own server, the VM stays in your subscription and your chosen region. We run Odoo on it: deploys from Git, staging, backups, updates and help from Odoo experts. You keep the VM running and big enough. Our software connects out from your VM, so you don't open any port to us.

Skysize is built by ex-Odoo engineers and is ISO 27001 certified.

Talk to an Odoo hosting expert

FAQ

Can Odoo run on Azure? Yes. Use a general purpose x86 VM with a Premium SSD disk, Postgres on the VM or on Azure Database for PostgreSQL, and backups to Blob Storage.

Can I use Azure Database for PostgreSQL for Odoo? Yes. Use private access, allow the pg_trgm extension, and create a database user for Odoo that can create databases. Connect on port 5432, not through PgBouncer. Server backups don't cover the filestore, so back that up too.

Which Azure VM size is best for Odoo? One with enough memory for your workers and Postgres, and a steady CPU. A D-series size such as Standard_D2s_v5 is a good start. Avoid B-series VMs for production use.

Can Odoo run on Azure Arm VMs? Odoo itself can, but check that wkhtmltopdf works before you go live. Our bring your own server software runs on x86 only, so pick an Intel or AMD size if you may use it.

What permissions does the Odoo VM need for backups? Only blob write and add on your backup container, through a custom role on the VM's managed identity. Restores use a second VM with Storage Blob Data Reader.

Do I need to open port 22? Not to the internet. Azure Bastion connects to the VM from inside the VNet, and you sign in with Entra ID.

Why doesn't Odoo send email from my Azure VM? Azure blocks port 25 on most subscription types. Send through your mail provider or Azure Communication Services on port 587.

Odoo on Azure: how to host it properly
Writes about Odoo infrastructure at Skysize, a Odoo hosting provider based in Belgium, made by ex-Odoo engineers and ISO 27001 certified, serving businesses and agencies worldwide.