# Deploy on Google Cloud

> Run Oatmilk on Compute Engine with Cloud SQL, Memorystore and Cloud Storage, step by step.

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

This guide runs Oatmilk on one Compute Engine VM, with its data in Google Cloud's managed services: Cloud SQL for PostgreSQL, Memorystore for Redis and Cloud Storage for files. Do every step in the same project and region, for example `northamerica-northeast1`.

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

## 1. Reserve an address and open the web ports

1. In **VPC network › IP addresses**, reserve a static external IPv4 address in your region.
2. In **VPC network › Firewall**, create a rule on the `default` network: ingress, target tag `oatmilk`, source `0.0.0.0/0`, TCP ports `80` and `443`.

## 2. Create the database

In **SQL › Create instance › PostgreSQL**:

1. **Database version:** PostgreSQL 17. Set a password for the `postgres` user and keep it.
2. **Machine:** 2 vCPUs or more, with automated backups on.
3. **Connections:** **Private IP** on the `default` network. If it asks, set up the private services connection. Turn **Public IP** off.

When it is ready:

1. In **Databases**, create `oatmilk`.
2. Copy the instance's **private IP address**.

```
postgres://postgres:<password>@<private IP>:5432/oatmilk?sslmode=require
```

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

## 3. Create Redis

In **Memorystore › Redis › Create instance**:

1. **Tier:** Basic (or Standard for a replica), 1 GB, in your region.
2. **Network:** `default`.
3. Turn **AUTH** on. Leave in-transit encryption off: the VM reaches Redis only on Google's private network.

When it is ready, copy its IP address and its AUTH string.

```
redis://:<auth string>@<IP address>:6379
```

## 4. Create the bucket for files

Cloud Storage serves an S3-compatible API, which Oatmilk's file storage uses.

1. In **Cloud Storage › Buckets**, create one, for example `acme-oatmilk-files`, in your region, with **uniform** access and **public access prevention** on.
2. In **IAM & Admin › Service accounts**, create `oatmilk-storage`. In the bucket's **Permissions**, give it **Storage Object Admin**.
3. In **Cloud Storage › Settings › Interoperability**, create an **HMAC key** for that service account. Copy the access ID and secret.

## 5. Create the VM

In **Compute Engine › VM instances › Create instance**:

1. **Machine:** `e2-standard-2` (2 vCPUs, 8 GB) or larger, in your region.
2. **Boot disk:** Ubuntu 24.04 LTS, 40 GB.
3. **Networking:** network tag `oatmilk`, and the static address from step 1 as its external IP.

## 6. Point your domain at it

In **Cloud DNS**, or at your DNS provider, add an `A` record for `books.example.com` with the static address. Wait until `dig +short books.example.com` answers with it.

## 7. Install Oatmilk on the VM

Connect with **SSH** from the console, 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 Cloud SQL connection string |
| Which Redis? | **Use one I already have**, then the Memorystore address |
| Where should files be kept? | **In an S3-compatible bucket**: the bucket name, region `auto`, endpoint `https://storage.googleapis.com`, the HMAC access ID and secret, and **Yes** to path-style addresses |
| 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
export OATMILK_S3_ACCESS_KEY_ID=GOOG…
export OATMILK_S3_SECRET_ACCESS_KEY=…
bun run self-host init --server --domain books.example.com \
  --database-url 'postgres://postgres:…@10.0.0.3:5432/oatmilk?sslmode=require' \
  --redis-url 'redis://:…@10.0.0.4:6379' \
  --s3-bucket acme-oatmilk-files --s3-region auto --s3-endpoint https://storage.googleapis.com --s3-force-path-style
bun run self-host up
```

## 8. 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. Upload a receipt to check that files reach the bucket.

## Back up

- **Database:** Cloud SQL's automated backups and point-in-time recovery.
- **Files:** turn on **Object versioning** or soft delete on the bucket.
- **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 |
| --- | --- |
| The database steps can't connect | Check that Cloud SQL has a private IP on the VM's network, and that the string ends in `?sslmode=require`. |
| The app can't reach Redis | Check the AUTH string and that Memorystore is on the `default` network. |
| Uploads fail with a signature error | Check endpoint `https://storage.googleapis.com`, region `auto` and path-style addresses in `self-host/.env`. |
