Self-hosting
Deploy on AWS
Run Oatmilk on EC2 with Amazon RDS for PostgreSQL, ElastiCache and S3, step by step.
This guide runs Oatmilk on one EC2 instance, with its data in AWS's managed services: Amazon RDS for PostgreSQL, ElastiCache for Redis or Valkey, and S3 for files. Do every step in the same region, for example ca-central-1.
1. Create two security groups
In EC2 › Security Groups, in your VPC (the default VPC is fine):
| Name | Inbound rules |
|---|---|
oatmilk-web | SSH (22) from your own IP address; HTTP (80) and HTTPS (443) from anywhere |
oatmilk-data | PostgreSQL (5432) and custom TCP 6379, both from the oatmilk-web security group |
The database and Redis only accept connections from the instance.
2. Create the database
In RDS › Create database:
- Standard create, engine PostgreSQL, version 17.
- Templates: Production, or Dev/Test for a trial.
- Master username:
postgres. Set a strong password and keep it. - Instance:
db.t4g.mediumor larger, with 20 GB of storage or more. - Connectivity: your VPC, Public access: No, security group
oatmilk-data. - Additional configuration › Initial database name:
oatmilk. Keep automated backups and encryption on.
When it is Available, copy its Endpoint. The connection string is:
postgres://postgres:<password>@<endpoint>:5432/oatmilk?sslmode=require3. Create Redis
In ElastiCache › Create cache:
- Choose Valkey or Redis OSS, then Design your own cache and Node-based cluster.
- Cluster mode: Disabled, node type
cache.t4g.small, one replica or none. - Your VPC's subnets, security group
oatmilk-data. - Encryption in transit: on. For access control, set an AUTH token.
When it is Available, copy the Primary endpoint. The address is:
rediss://:<auth token>@<primary endpoint>:63794. Create the bucket for files
In S3 › Create bucket, name it, for example acme-oatmilk-files, in your region. Keep Block all public access on.
In IAM › Users, create oatmilk-storage with no console access, and add this inline policy:
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": "arn:aws:s3:::acme-oatmilk-files" },
{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"], "Resource": "arn:aws:s3:::acme-oatmilk-files/*" }
]
}Under the user's Security credentials, create an access key for Application running outside AWS, and copy the key ID and secret.
Browsers never reach the bucket: Oatmilk hands out short-lived signed links on your own domain, so the bucket needs no public access and no CORS rules.
5. Launch the instance
In EC2 › Launch instance:
- Ubuntu Server 24.04 LTS, instance type
t3.large(2 CPUs, 8 GB) or larger. - A key pair for SSH, your VPC, a public subnet, security group
oatmilk-web. - 40 GB of
gp3storage.
Then, in Elastic IPs, allocate an address and associate it with the instance, so its address never changes.
6. Point your domain at it
In Route 53 › Hosted zones, or at your DNS provider, add an A record for books.example.com with the Elastic IP. Wait until dig +short books.example.com answers with it.
7. Install Oatmilk on the instance
SSH in, then:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker ubuntu && 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 RDS connection string |
| Which Redis? | Use one I already have, then the ElastiCache address |
| Where should files be kept? | In an S3-compatible bucket: the bucket name, your region, an empty endpoint, then the access key ID and secret |
| 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 VPC as Another OpenAI-compatible server |
| Should Oatmilk send email? | Yes, with Resend |
Or, without questions:
export OATMILK_S3_ACCESS_KEY_ID=AKIA…
export OATMILK_S3_SECRET_ACCESS_KEY=…
bun run self-host init --server --domain books.example.com \
--database-url 'postgres://postgres:…@<endpoint>:5432/oatmilk?sslmode=require' \
--redis-url 'rediss://:…@<primary endpoint>:6379' \
--s3-bucket acme-oatmilk-files --s3-region ca-central-1
bun run self-host upOn the first start, Oatmilk prepares the database (its roles and schemas) and applies every migration. The first build takes about 10 minutes.
8. Check it and sign in
bun run self-host doctor
bun run self-host logs db-init migrate # if preparing the database failedOpen https://books.example.com, sign in with the account setup made, and create your company. Upload a receipt to check that files reach S3.
Back up
- Database: RDS automated backups and snapshots.
bun run self-host backupdoesn't copy a database it doesn't run. - Files: turn on Versioning on the bucket, or replicate it to another region.
- Settings: keep a copy of
self-host/.envsomewhere safe. It holds the keys that decrypt connector credentials and contractor details; without it, a restored database can't read them.
If something goes wrong
| Problem | Fix |
|---|---|
| The database steps can't connect | Check that oatmilk-data allows 5432 from oatmilk-web, and that the string ends in ?sslmode=require. |
db-init says permission denied creating a role | Use the RDS master user. Oatmilk creates its own roles on the first start. |
| The app can't reach Redis | Check the rediss:// scheme, port 6379, the AUTH token, and that cluster mode is disabled. |
| Uploads fail | Check the bucket's region in self-host/.env and the IAM policy's bucket name. |