# Deploy on Azure

> Run Oatmilk on an Azure VM with Azure Database for PostgreSQL and Azure Cache for Redis, step by step.

Source: https://app.getoatmilk.com/docs/self-hosting/azure

This guide runs Oatmilk on one Azure virtual machine, with its database in Azure Database for PostgreSQL and, optionally, Redis in Azure Cache for Redis. Azure Blob Storage has no S3-compatible API, so files stay on the VM's disk or go to another S3-compatible store. Do every step in one resource group and region, for example `canadacentral`.

> [!NOTE]
> Oatmilk's own tests run the bundled stack in Docker. The managed services here speak the same protocols (PostgreSQL and Redis), but check each step's result as you go, and run `bun run self-host doctor` at the end.

## 1. Create the network and the VM

In **Virtual machines › Create**:

1. A new resource group, for example `oatmilk`, and your region.
2. **Image:** Ubuntu Server 24.04 LTS. **Size:** `Standard_B2ms` (2 vCPUs, 8 GB) or larger.
3. **Authentication:** an SSH public key.
4. **Inbound ports:** SSH (22), HTTP (80) and HTTPS (443).
5. **Disks:** a 64 GB Premium SSD OS disk. Files are kept here unless you choose a bucket.
6. **Networking:** a new virtual network with the default subnet, and a **Static** public IP.

## 2. Create the database

In **Azure Database for PostgreSQL flexible servers › Create**:

1. The same resource group and region. **PostgreSQL version:** 17.
2. **Compute + storage:** Burstable `B2s` for a trial, General Purpose for a team.
3. **Authentication:** PostgreSQL authentication only. Set an admin name, for example `oatmilk_admin`, and a password.
4. **Networking:** **Private access (VNet integration)**, in the VM's virtual network, on a new subnet for the database.

When it is deployed:

1. In **Server parameters**, find `azure.extensions` and allow `PGCRYPTO`, `UUID-OSSP` and `PG_STAT_STATEMENTS`. Save. Oatmilk's database needs the first two; Azure refuses extensions that aren't on this list.
2. In **Databases**, add `oatmilk`.
3. Copy the **Server name** from **Overview**.

```
postgres://oatmilk_admin:<password>@<server name>:5432/oatmilk?sslmode=require
```

Encode special characters in the password for a URL (`@` is `%40`), or use letters and digits.

## 3. Choose where Redis runs

Redis holds caches, rate limits and locks, so the simplest choice is the bundled Redis on the VM: answer **Run one here** in setup.

For a managed one, create an **Azure Cache for Redis** in the same region, then copy its host name and **Primary** access key from **Authentication**. Use its TLS port:

```
rediss://:<access key>@<name>.redis.cache.windows.net:6380
```

## 4. Choose where files go

- **On the VM** (the simplest): answer **On this machine** in setup. Files live in a Docker volume on the VM's disk. Back them up with `bun run self-host backup` and the VM's disk backups.
- **In an S3-compatible store** such as Cloudflare R2 or a MinIO you run: see [Bring your own services](https://app.getoatmilk.com/docs/self-hosting/services.md#files).

## 5. Point your domain at it

In **DNS zones**, or at your DNS provider, add an `A` record for `books.example.com` with the VM's public IP. Wait until `dig +short books.example.com` answers with it.

## 6. Install Oatmilk on the VM

SSH in, then:

```bash
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER" && newgrp docker
curl -fsSL https://bun.sh/install | bash && source ~/.bashrc
git clone https://github.com/AGI-Ventures-Canada/oatmilk.git && cd oatmilk
bun install
bun run self-host setup
```

| Question | Answer |
| --- | --- |
| Where will Oatmilk run? | **On a server**, at `books.example.com` |
| Which PostgreSQL database? | **Use one I already have**, then the connection string |
| Which Redis? | **Run one here**, or **Use one I already have** with the Azure Cache address |
| Where should files be kept? | **On this machine**, or your S3-compatible store |
| How should people sign in? | **Accounts kept here**, with sign-up **Only people I add or invite** |
| Which AI models…? | **Vercel AI Gateway**, or a model server in your network as **Another OpenAI-compatible server** |
| Should Oatmilk send email? | **Yes, with Resend** |

Or, without questions:

```bash
bun run self-host init --server --domain books.example.com \
  --database-url 'postgres://oatmilk_admin:…@<server name>:5432/oatmilk?sslmode=require'
bun run self-host up
```

## 7. Check it and sign in

```bash
bun run self-host doctor
bun run self-host logs db-init migrate   # if preparing the database failed
```

Open `https://books.example.com`, sign in with the account setup made, and create your company.

## Back up

- **Database:** the flexible server's automated backups and point-in-time restore.
- **Files on the VM:** `bun run self-host backup` on a schedule, copied off the VM, and Azure Backup for the VM's disk.
- **Settings:** keep a copy of `self-host/.env` somewhere safe. It holds the keys that decrypt connector credentials and contractor details.

## If something goes wrong

| Problem | Fix |
| --- | --- |
| `db-init` says an extension isn't allow-listed | Add `PGCRYPTO` and `UUID-OSSP` to `azure.extensions`, save, then `bun run self-host up` again. |
| The database steps can't connect | Check that the database is in the VM's virtual network, and that the string ends in `?sslmode=require`. |
| The app can't reach Azure Cache for Redis | Use `rediss://` and port 6380, with the access key as the password. |
