Self-hosting
Bring your own services
Use your own PostgreSQL, Redis and S3-compatible storage, such as Neon, Upstash, Cloudflare R2 or MinIO.
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:
postgres://<admin user>:<password>@<host>:5432/oatmilk?sslmode=require| Provider | Notes |
|---|---|
| Amazon RDS | The master user. See Deploy on AWS. |
| Google Cloud SQL | The postgres user. See Deploy on Google Cloud. |
| Azure Database for PostgreSQL | The admin user, with PGCRYPTO and UUID-OSSP allowed in azure.extensions. See Deploy on Azure. |
| 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.
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 |
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.
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-styleA 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.