# Bring your own services

> Use your own PostgreSQL, Redis and S3-compatible storage, such as Neon, Upstash, Cloudflare R2 or MinIO.

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

The Docker stack runs PostgreSQL, Redis and file storage for you. Any of the three can be a service you already run or rent instead, and the rest stays bundled. Setup asks about each one; choose **Use one I already have** or **In an S3-compatible bucket**.

> [!NOTE]
> Oatmilk's own tests run the bundled stack in Docker. Other services that speak the same protocols (PostgreSQL, Redis and S3) work the same way, but check each one with `bun run self-host doctor` after you start.

## Database

Oatmilk needs PostgreSQL 15 or newer, and a user that can create roles, schemas and the `pgcrypto` and `uuid-ossp` extensions. On the first start it creates the roles and schemas its migrations expect, then applies every migration. Use the provider's admin user, an empty database named `oatmilk`, and TLS:

```
postgres://<admin user>:<password>@<host>:5432/oatmilk?sslmode=require
```

| Provider | Notes |
| --- | --- |
| Amazon RDS | The master user. See [Deploy on AWS](https://app.getoatmilk.com/docs/self-hosting/aws.md#2-create-the-database). |
| Google Cloud SQL | The `postgres` user. See [Deploy on Google Cloud](https://app.getoatmilk.com/docs/self-hosting/google-cloud.md#2-create-the-database). |
| Azure Database for PostgreSQL | The admin user, with `PGCRYPTO` and `UUID-OSSP` allowed in `azure.extensions`. See [Deploy on Azure](https://app.getoatmilk.com/docs/self-hosting/azure.md#2-create-the-database). |
| Neon | Create a database named `oatmilk` and use its owner role. Copy the **direct** connection string, not the pooled one (its host has no `-pooler`). |
| Your own PostgreSQL | A superuser, or a user with `CREATEROLE` that owns the `oatmilk` database. |

Where the admin user can't grant the right to bypass row-level security, as on most managed services, Oatmilk's server role reads rows as the tables' owner instead. Nothing to set.

Encode special characters in the password for a URL (`@` is `%40`, `:` is `%3A`), or choose a password of letters and digits.

```bash title="Without questions"
bun run self-host init --server --domain books.example.com --database-url 'postgres://…/oatmilk?sslmode=require'
```

`bun run self-host backup` only copies the bundled database. With your own, use the provider's backups, or `pg_dump -Fc`.

## Redis

Any Redis 6 or newer, or Valkey, without cluster mode. It holds caches, rate limits and locks.

| Provider | Address |
| --- | --- |
| Upstash | `rediss://default:<password>@<name>.upstash.io:6379` |
| Amazon ElastiCache, cluster mode disabled | `rediss://:<auth token>@<primary endpoint>:6379` |
| Google Memorystore | `redis://:<auth string>@<IP address>:6379` |
| Azure Cache for Redis | `rediss://:<access key>@<name>.redis.cache.windows.net:6380` |
| Your own | `redis://:<password>@<host>:6379`, or `rediss://` with TLS |

```bash title="Without questions"
bun run self-host init --server --domain books.example.com --redis-url 'rediss://…'
```

## Files

Statements, receipts and documents go to a Docker volume on the machine, or to an S3-compatible bucket. Browsers never reach the bucket: Oatmilk hands out short-lived signed links on your own domain, so keep the bucket private, with no CORS rules.

Setup asks for the bucket, its region, the endpoint, an access key and whether to use path-style addresses.

| Provider | Region | Endpoint | Path-style |
| --- | --- | --- | --- |
| Amazon S3 | The bucket's region, such as `ca-central-1` | Leave empty | No |
| Cloudflare R2 | `auto` | `https://<account ID>.r2.cloudflarestorage.com` | Yes |
| Backblaze B2 | The bucket's region, such as `us-west-004` | `https://s3.us-west-004.backblazeb2.com` | Yes |
| Google Cloud Storage, with an HMAC key | `auto` | `https://storage.googleapis.com` | Yes |
| MinIO | `us-east-1` | `https://minio.example.internal:9000`, an address the Docker containers can reach | Yes |

The access key needs to read, write, list and delete objects in that one bucket. Give it no other permissions.

For a MinIO that serves plain `http`, also add `OATMILK_S3_PROTOCOL=http` to `self-host/.env`. Don't use `localhost` as its address: inside the storage container that means the container itself. Use the machine's network address instead.

```bash title="Without questions"
export OATMILK_S3_ACCESS_KEY_ID=…
export OATMILK_S3_SECRET_ACCESS_KEY=…
bun run self-host init --server --domain books.example.com \
  --s3-bucket oatmilk-files --s3-region auto --s3-endpoint https://<account ID>.r2.cloudflarestorage.com --s3-force-path-style
```

A MinIO on your own network keeps an [off-grid](https://app.getoatmilk.com/docs/self-hosting/off-grid.md) install off-grid.

## Change a service later

Run `bun run self-host setup` again. It asks every question again, and keeps the installation's secrets and the settings you added yourself in `self-host/.env`. Then `bun run self-host up`.

Changing the database or the file store doesn't move your data, so choose them before you add your books. A database can be moved with `pg_dump` and `pg_restore`.
