# Self-hosting Oatmilk > Run your own Oatmilk on a computer, a server or a cloud, with local AI models, your own services and day-to-day operations. # Self-host Oatmilk > Run your own Oatmilk on your computer, a server or a cloud, with your data and AI models staying with you. Source: https://app.getoatmilk.com/docs/self-hosting Oatmilk runs on your own hardware with everything it needs: the web app and its AI agents, PostgreSQL, file storage, Redis, scheduled jobs, sign-in, the REST API, MCP for AI apps and the [command line](https://app.getoatmilk.com/docs/terminal.md). Set up for it, nothing leaves the machine: accounts, books, statements, receipts and documents stay in your database and on your disk, and AI models can run on your own hardware too. One command asks a few questions, writes the settings, builds Oatmilk and starts it: ```bash bun run self-host setup ``` ## Choose a guide | You want | Follow | Time | | --- | --- | --- | | Oatmilk on your own computer, just for you | [Run it on your computer](https://app.getoatmilk.com/docs/self-hosting/your-computer.md) | 20 minutes | | Every AI model on your own hardware | [Use local AI models](https://app.getoatmilk.com/docs/self-hosting/local-models.md) | 15 minutes | | Nothing at all leaving the machine | [Go completely off-grid](https://app.getoatmilk.com/docs/self-hosting/off-grid.md) | 30 minutes | | A server for your team at your own domain | [Run it on a server](https://app.getoatmilk.com/docs/self-hosting/server.md) | 30 minutes | | Amazon Web Services with RDS, ElastiCache and S3 | [Deploy on AWS](https://app.getoatmilk.com/docs/self-hosting/aws.md) | 1 hour | | Google Cloud with Cloud SQL, Memorystore and Cloud Storage | [Deploy on Google Cloud](https://app.getoatmilk.com/docs/self-hosting/google-cloud.md) | 1 hour | | Microsoft Azure with its managed PostgreSQL and Redis | [Deploy on Azure](https://app.getoatmilk.com/docs/self-hosting/azure.md) | 1 hour | | Your own PostgreSQL, Redis or S3-compatible storage (Neon, Upstash, Cloudflare R2, MinIO…) | [Bring your own services](https://app.getoatmilk.com/docs/self-hosting/services.md) | 20 minutes | | A machine without Docker | [Run it without Docker](https://app.getoatmilk.com/docs/self-hosting/without-docker.md) | 1 hour | | To change Oatmilk's code | [Develop Oatmilk locally](https://app.getoatmilk.com/docs/self-hosting/develop.md) | 15 minutes | Then [manage sign-in and accounts](https://app.getoatmilk.com/docs/self-hosting/accounts.md), and [update, back up and troubleshoot](https://app.getoatmilk.com/docs/self-hosting/operate.md) your installation. ## What runs where Everything runs in Docker Compose, from the `self-host/` folder of Oatmilk's source. | Piece | What it is | Stays on your machine? | | --- | --- | --- | | Web app and agents | One container, `app`, with the web app, every AI agent and the five-minute jobs | Yes | | Database | PostgreSQL 17, bundled, or your own PostgreSQL 15 or newer | Yes | | Database API | PostgREST, the same piece Supabase runs. Never published. | Yes | | File storage | Supabase Storage, on a Docker volume or in an S3-compatible bucket | Yes, on the disk or MinIO | | Redis | Caches, rate limits and locks. Bundled, or your own. | Yes | | Sign-in | Accounts in your database with Better Auth, or Clerk | Yes, with Better Auth | | HTTPS | Caddy: its own certificate for `*.localhost`, Let's Encrypt for a real domain | Yes, on `*.localhost` | | AI models | Ollama, LM Studio or any OpenAI-compatible server, or Vercel AI Gateway | Yes, with local models | | Email | Off, or Resend for invoices, invitations and reminders | Off | | Bank, payment and data connections | Wise, Stripe, Notion, Google Drive, Microsoft: each optional, set up per company | Only when you connect them | ## What you need - **Oatmilk's source code**, cloned with Git. - **Docker** Desktop, or Docker Engine with Compose 2.20 or newer. Check with `docker compose version`. - **[Bun](https://bun.sh)** 1.3 or newer, which runs the setup commands. - **About 4 GB of memory and 15 GB of disk** for Oatmilk itself. Local AI models need more: see [Use local AI models](https://app.getoatmilk.com/docs/self-hosting/local-models.md#what-your-computer-needs). - **The internet, once,** to build the image. After that, Oatmilk can run without it. ## Use your Oatmilk like the hosted one A self-hosted Oatmilk has the same app, [REST API](https://app.getoatmilk.com/docs/quickstart.md), [webhooks](https://app.getoatmilk.com/docs/webhooks.md) and [MCP server](https://app.getoatmilk.com/docs/mcp.md). These docs are part of it too: open `/docs` on your own address, and every example uses that address. - **API keys** come from **Developers › API keys**: `oat_test_…` on an install for one computer, `oat_live_…` on a server. - **AI apps** connect to `https:///api/mcp` and sign in through your Oatmilk's own accounts. - **The CLI** signs in with `oatmilk login --host https://`. See [Sign in and choose a company](https://app.getoatmilk.com/docs/terminal/sign-in.md#your-own-oatmilk). > [!NOTE] > Self-hosted installs run the same code as the hosted Oatmilk. The settings that only matter to a self-hosted install are switched on by `OATMILK_DEPLOYMENT=self-hosted`, which the Compose stack sets for you. # Run it on your computer > Install Oatmilk on your own Mac, Linux or Windows computer, step by step, at oatmilk.localhost. Source: https://app.getoatmilk.com/docs/self-hosting/your-computer This guide installs a complete Oatmilk on your own computer, for you alone, at `https://oatmilk.localhost`. Everything runs in Docker, and your data stays in Docker volumes on your disk. ## 1. Install what it needs - **Docker.** [Docker Desktop](https://www.docker.com/products/docker-desktop/) on macOS and Windows; Docker Desktop or Docker Engine with the Compose plugin on Linux. Give Docker at least 4 GB of memory. - **Bun.** `curl -fsSL https://bun.sh/install | bash` (on Windows, inside WSL). - **Git.** Check them: ```bash docker compose version # 2.20 or newer bun --version ``` On Windows, run the commands in this guide in a WSL terminal, with Docker Desktop's WSL integration turned on. ## 2. Get Oatmilk ```bash git clone https://github.com/AGI-Ventures-Canada/oatmilk.git cd oatmilk bun install ``` ## 3. Run setup ```bash bun run self-host setup ``` Setup asks one question at a time. Use the arrow keys and `⏎`. For a personal install, answer: | Question | Answer | | --- | --- | | Where will Oatmilk run? | **On this computer** | | Address to open it at | `oatmilk.localhost` (press `⏎`) | | Use the standard web ports, 443 and 80? | **Yes**, unless another program uses them; then **No** and `8443` and `8080` | | Which PostgreSQL database? | **Run one here** | | Which Redis? | **Run one here** | | Where should files be kept? | **On this machine** | | How should people sign in? | **Accounts kept here** | | Who can make an account? | **Anyone who can open the site** (only your computer can) | | Which AI models should read documents and run Ask AI? | See below | | Should Oatmilk send email? | **No** | | Create your own account now? | **Yes**, then your email, name and a password of 8 characters or more | | Build and start Oatmilk now? | **Yes** | **AI models.** Oatmilk uses AI to read statements, receipts and documents, and for Ask AI. - To keep everything on your computer, choose **Ollama on this computer** or **LM Studio on this computer**, and set it up with [Use local AI models](https://app.getoatmilk.com/docs/self-hosting/local-models.md) before you upload documents. - To use hosted models, choose **Vercel AI Gateway** and paste an AI Gateway API key. - **None for now** works too: Oatmilk keeps your books, but doesn't read documents until you add models. Setup writes your settings to `self-host/.env`, builds the image and starts everything. The first build takes about 10 minutes. When it finishes, it creates your account and prints what to do next. ## 4. Trust the local certificate Oatmilk serves `https://oatmilk.localhost` with a certificate from its own local certificate authority. Until you trust that authority, your browser warns about the certificate. ```bash bun run self-host cert ``` It saves the authority to `self-host/oatmilk-local-ca.crt` and prints the command that trusts it on your system: | System | Command | | --- | --- | | macOS | `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain self-host/oatmilk-local-ca.crt` | | Linux | `sudo cp self-host/oatmilk-local-ca.crt /usr/local/share/ca-certificates/oatmilk-local-ca.crt && sudo update-ca-certificates` | | Windows | `certutil -addstore -f ROOT self-host\oatmilk-local-ca.crt`, in an administrator PowerShell opened in the `oatmilk` folder | On Windows with WSL, `cert` prints the Linux command, which trusts the file inside WSL only: run the Windows command too, so your Windows browser trusts it. Restart your browser afterwards. Firefox keeps its own list: in **Settings › Privacy & Security › Certificates › View Certificates › Authorities**, import the same file. ## 5. Sign in and create your company 1. Open . 2. Sign in with the email and password you gave setup. 3. Create your company: its name, then its profile. Upload a bank statement or a receipt to check that documents are read. With **None for now**, they wait until you add models. ## 6. Connect the command line (optional) ```bash export NODE_EXTRA_CA_CERTS="$PWD/self-host/oatmilk-local-ca.crt" npx @getoatmilk/cli login --host https://oatmilk.localhost npx @getoatmilk/cli ``` See [Sign in and choose a company](https://app.getoatmilk.com/docs/terminal/sign-in.md#your-own-oatmilk). ## Check it ```bash bun run self-host status # each service, and whether the site answers bun run self-host doctor # checks everything and says what to fix ``` ## Stop and start ```bash bun run self-host down # stops everything; your data stays bun run self-host up # starts it again ``` Oatmilk starts again on its own when Docker does, for example after a restart. ## If something goes wrong | Problem | Fix | | --- | --- | | The browser says the certificate isn't trusted | Run `bun run self-host cert`, then the command it prints, and restart the browser | | Ports 80 or 443 are taken | Run setup again and answer **No** to the standard ports, then open `https://oatmilk.localhost:8443` | | Another program can't open `oatmilk.localhost` | Add `127.0.0.1 oatmilk.localhost` to `/etc/hosts`. Browsers and the CLI don't need it. | | Setup stopped while building | Read the output above the error, fix it, then run `bun run self-host up` | | Your account wasn't created | `bun run self-host user add --email you@example.com --first-name Ada` | [Update, back up and troubleshoot](https://app.getoatmilk.com/docs/self-hosting/operate.md) has more. # Use local AI models > Run every model Oatmilk uses on your own hardware with Ollama, LM Studio, vLLM or llama.cpp. Source: https://app.getoatmilk.com/docs/self-hosting/local-models Oatmilk uses AI models to read statements, receipts and documents, to sort mail and match receipts, and for Ask AI. By default it calls hosted models through Vercel AI Gateway. Pointed at a model server on your own hardware instead, every one of those calls stays with you, nothing is billed, and no API key is needed. This works with [Ollama](https://ollama.com), [LM Studio](https://lmstudio.ai) and any server that speaks the OpenAI chat completions API, such as vLLM, llama.cpp's `llama-server` and LocalAI. ## What your computer needs A rough guide to the memory the models need, on top of Oatmilk's own 4 GB: | Setup | Memory | Good for | | --- | --- | --- | | A 9B chat model and a 4B classifier | 16 GB, more is better | Real books on one computer | | 4B models | 8 GB | Trying it out; slower and less accurate | | 0.8B models | 4 GB | Checking that everything is connected, not real use | | 30B models and larger, on a GPU server | 32 GB of GPU memory or more | A team, faster and more accurate | A Mac with Apple silicon, or an NVIDIA GPU, makes answers much faster than a CPU alone. ## Option 1: Ollama Use Ollama 0.35 or newer, which adds decision models that answer Oatmilk's classifier questions with a confidence, as the hosted classifier does. ### 1. Install Ollama and pull the models ```bash ollama pull qwen3.5:9b # chat, tools, extraction and receipt images ollama pull tev1 # classifiers: a decision model (nimble is larger and more accurate) ``` ### 2. Start it with a longer context Ollama loads models with a short context by default. Long statements and the agents need more: ```bash OLLAMA_CONTEXT_LENGTH=32768 ollama serve ``` On Linux, Oatmilk's container reaches your computer through Docker's network, not `127.0.0.1`, so Ollama must listen on every address: ```bash OLLAMA_HOST=0.0.0.0 OLLAMA_CONTEXT_LENGTH=32768 ollama serve ``` With Ollama's systemd service, set both with `sudo systemctl edit ollama` instead. Docker Desktop on macOS and Windows reaches Ollama either way. ### 3. Point Oatmilk at it In `bun run self-host setup`, choose **Ollama on this computer**, and keep `qwen3.5:9b` and `tev1` as the models. Setup writes: ```bash title="self-host/.env" OATMILK_MODEL_PROVIDER=ollama OATMILK_LOCAL_BASE_URL=http://host.docker.internal:11434/v1 OATMILK_LOCAL_MODEL=qwen3.5:9b OATMILK_LOCAL_CLASSIFIER_MODEL=tev1 OATMILK_LOCAL_CLASSIFIER_API=systemone ``` On an older Ollama, remove the last line, and the chat model answers the classifier questions too. ## Option 2: LM Studio 1. Download a model in LM Studio, for example **Qwen3.5 9B** (it reads images too) and, for classifiers, **Qwen3.5 4B**. Set each one's context length to 32768 or more when you load it. 2. Open the **Developer** tab and start the server, or run `lms server start`. It listens on port 1234. 3. On Linux, turn on **Serve on Local Network** in the server settings, so Oatmilk's container can reach it. 4. In `bun run self-host setup`, choose **LM Studio on this computer**, with the model identifiers LM Studio shows. ```bash title="self-host/.env" OATMILK_MODEL_PROVIDER=lmstudio OATMILK_LOCAL_BASE_URL=http://host.docker.internal:1234/v1 OATMILK_LOCAL_MODEL=qwen/qwen3.5-9b OATMILK_LOCAL_CLASSIFIER_MODEL=qwen/qwen3.5-4b OATMILK_LOCAL_CLASSIFIER_API=chat ``` ## Option 3: vLLM, llama.cpp or another server Any server with an OpenAI-compatible `/v1` address works, on this computer or another one on your network. For example, vLLM on a GPU server: ```bash vllm serve Qwen/Qwen3-8B --enable-auto-tool-choice --tool-call-parser hermes --max-model-len 32768 ``` In setup, choose **Another OpenAI-compatible server** and give its address, ending in `/v1`, and the model's name. An address typed as `localhost` is pointed back at your computer from inside the container. ```bash title="self-host/.env" OATMILK_MODEL_PROVIDER=openai-compatible OATMILK_LOCAL_BASE_URL=http://192.168.1.50:8000/v1 OATMILK_LOCAL_MODEL=Qwen/Qwen3-8B OATMILK_LOCAL_API_KEY= # only if your server asks for one ``` Choose a model that supports tool calling and structured output, and start the server with tool calling turned on. ## Change models later Edit `self-host/.env`, then apply it: ```bash bun run self-host up bun run self-host doctor # checks that the app container reaches the model server ``` ## Settings | Setting | Default | What it does | | --- | --- | --- | | `OATMILK_MODEL_PROVIDER` | `gateway` | `gateway`, `ollama`, `lmstudio` or `openai-compatible` | | `OATMILK_LOCAL_BASE_URL` | Ollama's or LM Studio's usual address | The server's `/v1` address. Required for `openai-compatible`. | | `OATMILK_LOCAL_API_KEY` | none | Sent as a bearer token when set | | `OATMILK_LOCAL_MODEL` | required | Answers every chat, tool, extraction and review call | | `OATMILK_LOCAL_VISION_MODEL` | the chat model | Used whenever a prompt has an image, a PDF page or another file | | `OATMILK_LOCAL_REASONING` | each call's own | Caps thinking: `none`, `minimal`, `low`, `medium`, `high` or `xhigh`. `none` answers fastest on a laptop. | | `OATMILK_LOCAL_CLASSIFIER_MODEL` | the chat model | Answers classifier questions: document kinds, mail routing, receipt matches, categories | | `OATMILK_LOCAL_CLASSIFIER_API` | `chat` | `systemone` for an Ollama decision model, `chat` to ask a chat model | | `OATMILK_LOCAL_CLASSIFIER_BASE_URL` | `OATMILK_LOCAL_BASE_URL` | When classifiers run on another server, such as Ollama next to LM Studio | | `OATMILK_LOCAL_EMBEDDING_MODEL` | none | For embeddings, for example `nomic-embed-text` | | `OATMILK_LOCAL_TRANSCRIPTION_MODEL` | none | Voice input in Ask AI, from a server with `/audio/transcriptions` such as a whisper.cpp server | | `OATMILK_LOCAL_TRANSCRIPTION_BASE_URL` | `OATMILK_LOCAL_BASE_URL` | When transcription runs on another server | | `OATMILK_LOCAL_MODEL_MAP` | none | Pins particular hosted model ids to particular local models, such as `typesafe-ai/jev=qwen3:4b` | An invalid setting stops Oatmilk at start-up with a message that names the setting to fix. ## Classifiers and automatic matching Oatmilk's classifiers answer typed questions with probabilities. An Ollama decision model (`systemone`) also reports a confidence for each answer, the way the hosted classifier does, so automatic steps such as matching a receipt to a bank line keep working: they still need 98% probability and 95% confidence. A chat model (`chat`) gives its own estimate of the probabilities but no confidence. Every classifier still works, but steps that need confidence, such as automatic receipt matching, leave the decision to a person. ## Which models to choose | Role | Ollama | LM Studio | | --- | --- | --- | | Chat, tools and extraction | `qwen3.5:9b` (smaller: `qwen3.5:4b`, `gemma4:e4b`) | `qwen/qwen3.5-9b`, `google/gemma-4-e4b` | | Classifiers | `nimble` or `tev1`, with `systemone` | `qwen/qwen3.5-4b`, with `chat` | | Receipts and documents (vision) | `qwen3.5:9b`, `gemma4:12b` | `qwen/qwen3.5-9b`, `google/gemma-4-12b` | | Embeddings | `nomic-embed-text` | nomic-embed-text v1.5 | Qwen3.5 and similar models think before they answer. On slow hardware, set `OATMILK_LOCAL_REASONING=none`. ## Limits - Quality depends on the model. Check a few real statements and receipts before you trust a model with your books. - Image generation isn't available locally, and voice input needs a transcription server. - Some agents look up their model's context window in a public catalog. If you run fully offline and an agent doesn't start, pin its model to a local one with `OATMILK_LOCAL_MODEL_MAP` and check `bun run self-host logs app`. ## Without the Docker stack The same settings work for `bun run dev` in `.env.local`. There, Oatmilk runs on your computer itself, so use `http://localhost:11434/v1` (Ollama) or `http://localhost:1234/v1` (LM Studio), which are also the defaults. See [Develop Oatmilk locally](https://app.getoatmilk.com/docs/self-hosting/develop.md). # Go completely off-grid > A 100% local Oatmilk: accounts, books, files and AI models on one machine, even with no internet. Source: https://app.getoatmilk.com/docs/self-hosting/off-grid An off-grid Oatmilk keeps everything on one machine: accounts in your own database, books and files on your disk, and AI models on your own hardware. Once it is built, it runs with the network cable unplugged. ## 1. Set up local models first Follow [Use local AI models](https://app.getoatmilk.com/docs/self-hosting/local-models.md) to install Ollama or LM Studio and pull the models, then check that the server answers: ```bash curl http://localhost:11434/v1/models # Ollama; LM Studio is on port 1234 ``` ## 2. Run setup with every piece local ```bash git clone https://github.com/AGI-Ventures-Canada/oatmilk.git cd oatmilk bun install bun run self-host setup ``` | Question | Off-grid answer | | --- | --- | | Where will Oatmilk run? | **On this computer**, at `oatmilk.localhost` | | Which PostgreSQL database? | **Run one here** | | Which Redis? | **Run one here** | | Where should files be kept? | **On this machine** | | How should people sign in? | **Accounts kept here** | | Which AI models…? | **Ollama on this computer** or **LM Studio on this computer** | | Should Oatmilk send email? | **No** | After writing the settings, setup confirms: "Everything stays on this machine: accounts, files, the database and AI models." If it doesn't say so, one of the answers sends something elsewhere. Then trust the local certificate as in [Run it on your computer](https://app.getoatmilk.com/docs/self-hosting/your-computer.md#4-trust-the-local-certificate), and sign in at . ## 3. Check that nothing leaves the machine ```bash bun run self-host doctor ``` It checks every service, the address, the certificate and that the app reaches your model server. Then turn off Wi-Fi and upload a receipt: it is read and categorized by your local models. ## What works with no internet - Reading and categorizing statements, receipts and documents. - Ask AI, with your local models. - The five-minute background jobs. - Team members, roles, invitations and outside accountants. Without email, copy an invitation link from **Settings › Team** and send it yourself. - The REST API, API keys, MCP for AI apps on the same machine or network, and the [command line](https://app.getoatmilk.com/docs/terminal.md). - These developer docs, at `https://oatmilk.localhost/docs`. ## What needs the internet These stay off until you turn them on, each in **Settings** for a company or in `self-host/.env`: - live bank feeds and payouts (Wise), Stripe, and data connections (Notion, Google Drive, Microsoft); - email in and out; - hosted AI models (Vercel AI Gateway), and web research for compliance; - Discord. ## Install on a machine that never goes online Building the image needs the internet once: it downloads Docker's base images, the npm packages, and the public list of AI models the agents' build reads. To install on a machine that never connects, build on one that does and carry the images across. On the connected machine, in an Oatmilk checkout with your settings: ```bash bun run self-host up # builds oatmilk:better-auth and pulls the other images bun run self-host down docker save -o oatmilk-images.tar $(docker compose -f self-host/compose.yaml config --images | sort -u) ``` Copy `oatmilk-images.tar`, the Oatmilk folder (with `node_modules` and `self-host/.env`) and your model files to the offline machine. Ollama keeps its models in `~/.ollama/models`. Then, on the offline machine: ```bash docker load -i oatmilk-images.tar echo "OATMILK_IMAGE=oatmilk:better-auth" >> self-host/.env # use the loaded image instead of building bun run self-host up bun run self-host user add --email you@example.com --first-name Ada # the database here starts empty ``` With `OATMILK_IMAGE` set, `up` starts the image you loaded and never tries to build. Remove that line when you want to build a newer version. ## Keep it safe An off-grid install is only as safe as the one machine it runs on. - Run `bun run self-host backup` on a schedule, and copy `self-host/backups` and `self-host/.env` to another disk. The `.env` file holds the keys that decrypt connector credentials and contractor details, so keep it as private as the backups. - Turn on two-step sign-in in your profile. - Use full-disk encryption (FileVault, BitLocker or LUKS). See [Update, back up and troubleshoot](https://app.getoatmilk.com/docs/self-hosting/operate.md#back-up). # Run it on a server > Host Oatmilk for your team on a VPS or your own server at your domain, with HTTPS from Let's Encrypt. Source: https://app.getoatmilk.com/docs/self-hosting/server This guide puts Oatmilk on one Linux server at your own domain, such as `books.example.com`, for you and your team. It works the same on Hetzner, DigitalOcean, Linode, OVH, a homelab machine or any other host with Docker. Everything runs on the server; for managed databases, see the [AWS](https://app.getoatmilk.com/docs/self-hosting/aws.md), [Google Cloud](https://app.getoatmilk.com/docs/self-hosting/google-cloud.md) and [Azure](https://app.getoatmilk.com/docs/self-hosting/azure.md) guides. ## 1. Get a server - Ubuntu 24.04 or another current Linux. - 2 CPUs, 8 GB of memory and 40 GB of disk. The build needs the memory; Oatmilk itself runs in about 4 GB. - A public IP address, with ports 80 and 443 open to the internet. Log in with SSH as a user that can run `sudo`. ## 2. Point your domain at it At your DNS provider, add an `A` record (and an `AAAA` record for IPv6) for the name people will open, pointing at the server's IP address: ``` books.example.com. A 203.0.113.10 ``` Check it from your own computer before you continue. Caddy can only get a certificate once the name leads to the server. ```bash dig +short books.example.com ``` ## 3. Install Docker, Bun and Git ```bash curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker "$USER" # then log out and back in curl -fsSL https://bun.sh/install | bash sudo apt-get install -y git docker compose version # 2.20 or newer ``` If the server has a firewall, open the web ports: ```bash sudo ufw allow 22/tcp && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw enable ``` ## 4. Get Oatmilk and run setup ```bash 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** | | Domain people will open | `books.example.com` | | Use the standard web ports, 443 and 80? | **Yes** | | Which PostgreSQL database? | **Run one here**, or your own (see [Bring your own services](https://app.getoatmilk.com/docs/self-hosting/services.md)) | | Which Redis? | **Run one here** | | Where should files be kept? | **On this machine**, or an S3-compatible bucket | | How should people sign in? | **Accounts kept here** | | Who can make an account? | **Only people I add or invite** | | Which AI models…? | **Vercel AI Gateway** with an API key, or a model server your team runs (see [Use local AI models](https://app.getoatmilk.com/docs/self-hosting/local-models.md)) | | Should Oatmilk send email? | **Yes, with Resend**, with your Resend API key and the domain you send from | | Create your own account now? | **Yes** | | Build and start Oatmilk now? | **Yes** | The first build takes about 10 minutes. Caddy gets a Let's Encrypt certificate the first time someone opens the site. > [!TIP] > To script it instead, `bun run self-host init --server --domain books.example.com` writes the settings without questions, and `bun run self-host up` starts it. `bun run self-host help` lists every option. ## 5. Sign in and invite your team 1. Open `https://books.example.com` and sign in with the account setup made. 2. Create your company and fill in its profile. 3. Invite people from **Settings › Team**. They get an email with a link, and make their account from it. Sign-up is closed, so only people you invite, or add yourself, can make an account: ```bash bun run self-host user add --email ada@example.com --first-name Ada --last-name Lovelace ``` Turn on two-step sign-in for administrators in their profile. [Sign-in and accounts](https://app.getoatmilk.com/docs/self-hosting/accounts.md) covers the rest. ## 6. Set up email (recommended) Without email, people only get invitations and reminders when you copy the link and send it yourself. With [Resend](https://resend.com): 1. Add your sending domain in Resend, such as `books.example.com`, and add the DNS records it shows. 2. Wait until Resend says the domain is verified. 3. Run setup again and answer **Yes, with Resend**, or add these to `self-host/.env` and run `bun run self-host up`: ```bash title="self-host/.env" ACCOUNTING_EMAIL_ENABLED=true RESEND_API_KEY=re_… OATMILK_EMAIL_DOMAIN=books.example.com ``` ## 7. Connect your tools - **API keys** from **Developers › API keys** start with `oat_live_`, and the API reference at `https://books.example.com/docs/api` uses your address. - **AI apps:** add `https://books.example.com/api/mcp` as a remote MCP server. - **The CLI:** `npx @getoatmilk/cli login --host https://books.example.com`. ## Keep it running | Task | Command | | --- | --- | | Check its health | `bun run self-host doctor` | | Update to a new version | `git pull`, then `bun run self-host up` | | Back up the database and files | `bun run self-host backup` | Schedule backups with cron, and copy them off the server: ```bash crontab -e # 30 3 * * * cd /home/me/oatmilk && /home/me/.bun/bin/bun run self-host backup ``` See [Update, back up and troubleshoot](https://app.getoatmilk.com/docs/self-hosting/operate.md). ## If something goes wrong | Problem | Fix | | --- | --- | | The site has no certificate | The domain doesn't point at the server yet, or ports 80 and 443 are closed. Check with `dig` and `bun run self-host logs caddy`. | | A teammate can't make an account | Sign-up is closed: invite them in **Settings › Team**, or `bun run self-host user add`. | | Invitation emails don't arrive | Check that Resend shows the domain as verified, and read `bun run self-host logs app`. You can always copy the invitation link from **Settings › Team**. | | The build stops with "killed" | The server ran out of memory. Use 8 GB, or add swap while building. | # Deploy on AWS > Run Oatmilk on EC2 with Amazon RDS for PostgreSQL, ElastiCache and S3, step by step. Source: https://app.getoatmilk.com/docs/self-hosting/aws 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`. > [!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. 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**: 1. **Standard create**, engine **PostgreSQL**, version 17. 2. **Templates:** Production, or Dev/Test for a trial. 3. **Master username:** `postgres`. Set a strong password and keep it. 4. **Instance:** `db.t4g.medium` or larger, with 20 GB of storage or more. 5. **Connectivity:** your VPC, **Public access: No**, security group `oatmilk-data`. 6. **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:@:5432/oatmilk?sslmode=require ``` > [!WARNING] > Encode special characters in the password for a URL: `@` becomes `%40`, `:` becomes `%3A`, `/` becomes `%2F`. Or choose a password of letters and digits. ## 3. Create Redis In **ElastiCache › Create cache**: 1. Choose **Valkey** or **Redis OSS**, then **Design your own cache** and **Node-based cluster**. 2. **Cluster mode: Disabled**, node type `cache.t4g.small`, one replica or none. 3. Your VPC's subnets, security group `oatmilk-data`. 4. **Encryption in transit: on.** For access control, set an **AUTH token**. When it is **Available**, copy the **Primary endpoint**. The address is: ``` rediss://:@:6379 ``` ## 4. 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: ```json title="oatmilk-storage 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**: 1. **Ubuntu Server 24.04 LTS**, instance type `t3.large` (2 CPUs, 8 GB) or larger. 2. A key pair for SSH, your VPC, a public subnet, security group `oatmilk-web`. 3. 40 GB of `gp3` storage. 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: ```bash 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: ```bash 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:…@:5432/oatmilk?sslmode=require' \ --redis-url 'rediss://:…@:6379' \ --s3-bucket acme-oatmilk-files --s3-region ca-central-1 bun run self-host up ``` On 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 ```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 S3. ## Back up - **Database:** RDS automated backups and snapshots. `bun run self-host backup` doesn'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/.env` somewhere 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. | # 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:@: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://:@: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`. | # Deploy on Azure > Run Oatmilk on an Azure VM with Azure Database for PostgreSQL and Azure Cache for Redis, step by step. Source: https://app.getoatmilk.com/docs/self-hosting/azure This guide runs Oatmilk on one Azure virtual machine, with its database in Azure Database for PostgreSQL and, optionally, Redis in Azure Cache for Redis. Azure Blob Storage has no S3-compatible API, so files stay on the VM's disk or go to another S3-compatible store. Do every step in one resource group and region, for example `canadacentral`. > [!NOTE] > Oatmilk's own tests run the bundled stack in Docker. The managed services here speak the same protocols (PostgreSQL and Redis), but check each step's result as you go, and run `bun run self-host doctor` at the end. ## 1. Create the network and the VM In **Virtual machines › Create**: 1. A new resource group, for example `oatmilk`, and your region. 2. **Image:** Ubuntu Server 24.04 LTS. **Size:** `Standard_B2ms` (2 vCPUs, 8 GB) or larger. 3. **Authentication:** an SSH public key. 4. **Inbound ports:** SSH (22), HTTP (80) and HTTPS (443). 5. **Disks:** a 64 GB Premium SSD OS disk. Files are kept here unless you choose a bucket. 6. **Networking:** a new virtual network with the default subnet, and a **Static** public IP. ## 2. Create the database In **Azure Database for PostgreSQL flexible servers › Create**: 1. The same resource group and region. **PostgreSQL version:** 17. 2. **Compute + storage:** Burstable `B2s` for a trial, General Purpose for a team. 3. **Authentication:** PostgreSQL authentication only. Set an admin name, for example `oatmilk_admin`, and a password. 4. **Networking:** **Private access (VNet integration)**, in the VM's virtual network, on a new subnet for the database. When it is deployed: 1. In **Server parameters**, find `azure.extensions` and allow `PGCRYPTO`, `UUID-OSSP` and `PG_STAT_STATEMENTS`. Save. Oatmilk's database needs the first two; Azure refuses extensions that aren't on this list. 2. In **Databases**, add `oatmilk`. 3. Copy the **Server name** from **Overview**. ``` postgres://oatmilk_admin:@:5432/oatmilk?sslmode=require ``` Encode special characters in the password for a URL (`@` is `%40`), or use letters and digits. ## 3. Choose where Redis runs Redis holds caches, rate limits and locks, so the simplest choice is the bundled Redis on the VM: answer **Run one here** in setup. For a managed one, create an **Azure Cache for Redis** in the same region, then copy its host name and **Primary** access key from **Authentication**. Use its TLS port: ``` rediss://:@.redis.cache.windows.net:6380 ``` ## 4. Choose where files go - **On the VM** (the simplest): answer **On this machine** in setup. Files live in a Docker volume on the VM's disk. Back them up with `bun run self-host backup` and the VM's disk backups. - **In an S3-compatible store** such as Cloudflare R2 or a MinIO you run: see [Bring your own services](https://app.getoatmilk.com/docs/self-hosting/services.md#files). ## 5. Point your domain at it In **DNS zones**, or at your DNS provider, add an `A` record for `books.example.com` with the VM's public IP. Wait until `dig +short books.example.com` answers with it. ## 6. Install Oatmilk on the VM SSH in, 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 connection string | | Which Redis? | **Run one here**, or **Use one I already have** with the Azure Cache address | | Where should files be kept? | **On this machine**, or your S3-compatible store | | 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 bun run self-host init --server --domain books.example.com \ --database-url 'postgres://oatmilk_admin:…@:5432/oatmilk?sslmode=require' bun run self-host up ``` ## 7. 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. ## Back up - **Database:** the flexible server's automated backups and point-in-time restore. - **Files on the VM:** `bun run self-host backup` on a schedule, copied off the VM, and Azure Backup for the VM's disk. - **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 | | --- | --- | | `db-init` says an extension isn't allow-listed | Add `PGCRYPTO` and `UUID-OSSP` to `azure.extensions`, save, then `bun run self-host up` again. | | The database steps can't connect | Check that the database is in the VM's virtual network, and that the string ends in `?sslmode=require`. | | The app can't reach Azure Cache for Redis | Use `rediss://` and port 6380, with the access key as the password. | # 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://:@: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:@.upstash.io:6379` | | Amazon ElastiCache, cluster mode disabled | `rediss://:@:6379` | | Google Memorystore | `redis://:@:6379` | | Azure Cache for Redis | `rediss://:@.redis.cache.windows.net:6380` | | Your own | `redis://:@: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://.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://.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`. # Sign-in and accounts > Choose how people sign in to your Oatmilk, who may make an account, and how to manage accounts. Source: https://app.getoatmilk.com/docs/self-hosting/accounts A self-hosted Oatmilk keeps its accounts in your own database by default, with [Better Auth](https://better-auth.com). It can use Clerk, a hosted sign-in service, instead. Either way, people's roles and companies are kept in Oatmilk itself. | | Accounts kept here (Better Auth) | Clerk | | --- | --- | --- | | Where accounts live | Your database, in the `better_auth` schema | Clerk's service | | Sign-in | Email and password, two-step codes from an authenticator app, backup codes | Everything Clerk offers, such as Google sign-in and passkeys | | AI apps and the CLI sign in through | Your Oatmilk, at `/api/auth` | Clerk | | Works with no internet | Yes | No | | Choose it | **Accounts kept here** in setup, or `init --auth better-auth` | **Clerk** in setup with its two keys, or `init --auth clerk` | Changing the choice rebuilds the image with `bun run self-host up`. Accounts don't move between the two. ## Who can make an account | Setting | Who | Default | | --- | --- | --- | | `OATMILK_AUTH_SIGNUP=closed` | Only people you add, and people with an invitation: to a team, as an outside accountant, or as a contractor | On a server | | `OATMILK_AUTH_SIGNUP=open` | Anyone who can open the site | On your own computer | On a server, keep sign-up closed unless you mean to run an open service. Open sign-up asks each new person to confirm their email before they can sign in or create a company, so it needs [email](https://app.getoatmilk.com/docs/self-hosting/server.md#6-set-up-email-recommended). Without email, open sign-up stops and asks the person to contact you. ## Add people The usual way is an invitation from **Settings › Team** in Oatmilk. With email on, the person gets a link; without it, copy the link and send it yourself. The person makes their account from the link and joins your company with the role you chose. From the server's command line: ```bash bun run self-host user add --email ada@example.com --first-name Ada --last-name Lovelace # asks for a password bun run self-host user list bun run self-host user reset-password --email ada@example.com # signs them out everywhere ``` A person added this way can sign in, then create a company or accept an invitation. In scripts, pipe the password in: `echo "$PASSWORD" | bun run self-host user add --email …`. Each person can create up to three companies. ## What people manage themselves In their profile, each person changes their name and password, turns on two-step sign-in, prints backup codes, and signs out devices they no longer use. Ask administrators to turn on two-step sign-in. ## Sign-in for AI apps, the CLI and the API With accounts kept here, your Oatmilk runs its own sign-in server for apps, at `/api/auth`: - **AI apps** connected to `https:///api/mcp` open your Oatmilk's consent page, where the person approves them. - **The CLI** signs in with `oatmilk login --host https://`, the same way. See [Sign in and choose a company](https://app.getoatmilk.com/docs/terminal/sign-in.md#your-own-oatmilk). - **API keys** from **Developers › API keys** work as on the hosted Oatmilk: `oat_test_…` on an install for one computer, `oat_live_…` on a server. ## Use Clerk instead 1. Create an application in Clerk, and copy its publishable key and secret key. 2. In Clerk, allow your Oatmilk's address as an origin, and turn on **Organizations**. 3. Run setup and choose **Clerk**, or: ```bash bun run self-host init --server --domain books.example.com --auth clerk # add --force to rewrite an existing self-host/.env ``` Then add both keys to `self-host/.env` and start it: ```bash title="self-host/.env" NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_… CLERK_SECRET_KEY=sk_live_… ``` ```bash bun run self-host up ``` `bun run self-host user` only manages accounts kept here. With Clerk, manage people in Clerk's dashboard and invite them in Oatmilk. # Update, back up and troubleshoot > Keep a self-hosted Oatmilk healthy: updates, backups, restores, logs, security and common fixes. Source: https://app.getoatmilk.com/docs/self-hosting/operate Every command runs from the Oatmilk folder. | Command | Does | | --- | --- | | `bun run self-host status` | Each service, and whether the site answers | | `bun run self-host doctor` | Checks Docker, the settings, the address, the certificate, the site and the model server, and says what to fix | | `bun run self-host logs app` | Follows one service's logs: `app`, `db`, `storage`, `postgrest`, `caddy`, `redis`, `db-init` or `migrate` | | `bun run self-host up` | Builds after an update and starts everything; new migrations apply on the way | | `bun run self-host migrate` | Applies new migrations without restarting | | `bun run self-host backup` | Saves the bundled database and the files to `self-host/backups` | | `bun run self-host cert` | Saves the local certificate authority for `*.localhost` and says how to trust it | | `bun run self-host down` | Stops everything; your data stays in Docker volumes | ## Update ```bash bun run self-host backup git pull bun install bun run self-host up ``` `up` rebuilds the image, applies any new database migrations, then starts the new version. The site is down for the minute or so the app takes to start. ## Back up ```bash bun run self-host backup ``` It writes two files to `self-host/backups`, readable only by you: - `oatmilk-