Self-hosting

Bring your own services

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

20 minutes · Intermediate

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.

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:

Text
postgres://<admin user>:<password>@<host>:5432/oatmilk?sslmode=require
ProviderNotes
Amazon RDSThe master user. See Deploy on AWS.
Google Cloud SQLThe postgres user. See Deploy on Google Cloud.
Azure Database for PostgreSQLThe admin user, with PGCRYPTO and UUID-OSSP allowed in azure.extensions. See Deploy on Azure.
NeonCreate 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 PostgreSQLA 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.

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.

ProviderAddress
Upstashrediss://default:<password>@<name>.upstash.io:6379
Amazon ElastiCache, cluster mode disabledrediss://:<auth token>@<primary endpoint>:6379
Google Memorystoreredis://:<auth string>@<IP address>:6379
Azure Cache for Redisrediss://:<access key>@<name>.redis.cache.windows.net:6380
Your ownredis://:<password>@<host>:6379, or rediss:// with TLS
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.

ProviderRegionEndpointPath-style
Amazon S3The bucket's region, such as ca-central-1Leave emptyNo
Cloudflare R2autohttps://<account ID>.r2.cloudflarestorage.comYes
Backblaze B2The bucket's region, such as us-west-004https://s3.us-west-004.backblazeb2.comYes
Google Cloud Storage, with an HMAC keyautohttps://storage.googleapis.comYes
MinIOus-east-1https://minio.example.internal:9000, an address the Docker containers can reachYes

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.

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 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.