---
title: "Self-host quick start"
description: "Run an oxabl cache on one machine with the container image, create the first tenant, and check that it's healthy."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.oxabl.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-host quick start

By the end of this page, a cache server runs in a container on one machine, with its metadata in SQLite and its data on a volume, and you've created its first tenant. You need Docker (or Podman, with the same commands) and Git.

## 1. Build the image

The image isn't published yet, so build it from the repository:

```sh
git clone https://github.com/oxabl-project/oxabl-cache
cd oxabl-cache
docker build -f deploy/container/Containerfile -t oxabl-cache-server .
```

The image holds two programs, `oxabl-cache-server` and `oxabl-cache-admin`, on a base with no shell. It runs as user `65532`, can run with a read-only root file system, and writes only to `/var/lib/oxabl-cache`. Its ports are `8443` (the API), `9091` (the admin API) and `9090` (health and metrics).

## 2. Write the config

Copy the example config next to where you'll run the server:

```sh
cp deploy/container/server.example.toml server.toml
```

```toml title="server.toml"
[server]
listen = "0.0.0.0:8443"
admin_listen = "0.0.0.0:9091"
metrics_listen = "0.0.0.0:9090"
public_url = "https://cache.example.com"

[tls]
terminated_upstream = true

[metadata]
url = "sqlite:///var/lib/oxabl-cache/meta.db"

[store]
kind = "fs"
root = "/var/lib/oxabl-cache/objects"

[keys]
url_key_file = "/var/lib/oxabl-cache/keys/url-key"
claim_hash_key_file = "/var/lib/oxabl-cache/keys/claim-key"

[log]
level = "info"
format = "json"
```

Set `public_url` to the address clients will use. `terminated_upstream = true` says TLS ends at a proxy in front of the container, which step 6 sets up. The server refuses to listen on a public address without one or the other:

```text
oxabl-cache-server: server.listen 0.0.0.0:8443 is not a loopback address: configure [tls] cert_file and key_file, or set tls.terminated_upstream = true behind a TLS proxy
```

## 3. Initialize

`init` creates the two key files, sets up the database, and prints the first operator token. It prints it once, so send it straight to a file only you can read:

```sh
docker volume create oxabl-cache-data
umask 077
docker run --rm --read-only --cap-drop ALL --security-opt no-new-privileges \
  -v oxabl-cache-data:/var/lib/oxabl-cache \
  -v "$PWD/server.toml:/etc/oxabl-cache/server.toml:ro" \
  oxabl-cache-server init > operator-token
```

```text
created key file /var/lib/oxabl-cache/keys/url-key
created key file /var/lib/oxabl-cache/keys/claim-key
operator token printed once above; it expires at unix time 1825278042
```

The token went to `operator-token`; the messages above go to the terminal. Move the token to your secret manager. A second `init` refuses:

```text
oxabl-cache-server: an operator token already exists; init mints the first one only
```

## 4. Start the server

```sh
docker run -d --name oxabl-cache --restart unless-stopped \
  --read-only --cap-drop ALL --security-opt no-new-privileges \
  -v oxabl-cache-data:/var/lib/oxabl-cache \
  -v "$PWD/server.toml:/etc/oxabl-cache/server.toml:ro" \
  -p 127.0.0.1:8443:8443 -p 127.0.0.1:9091:9091 -p 127.0.0.1:9090:9090 \
  oxabl-cache-server
```

All three ports are published on the host's loopback only. The admin and metrics ports must never be public; only the proxy in step 6 talks to `8443`.

The server prints a `ready` line once it's listening:

```sh
docker logs oxabl-cache
```

```text
{"event":"gc_leader_acquired","level":"info","ts":1793742043332}
{"ready":{"admin_listen":"0.0.0.0:9091","listen":"0.0.0.0:8443","metrics_listen":"0.0.0.0:9090","public_url":"https://cache.example.com"}}
```

## 5. Check that it's healthy

```sh
curl -f http://127.0.0.1:9090/readyz
```

`/readyz` answers `200` when the database and the store both respond. `/livez` on the same port answers `200` while the process is up. The image has no shell, so these checks run from the host.

For the details, ask the admin API. `oxabl-cache-admin` is in the image, and reads the token from standard input here, so it never appears in a command line:

```sh
docker exec -i oxabl-cache oxabl-cache-admin \
  --server http://127.0.0.1:9091 --token-file /dev/stdin health < operator-token
```

```text
metadata   ok
store      ok
gc_leader  true
```

## 6. Put TLS in front

The server has no built-in certificate renewal. Put a proxy in front that has it, such as [Caddy](https://caddyserver.com). This `Caddyfile` gets a certificate for the name and forwards to the server:

```text title="Caddyfile"
cache.example.com {
	reverse_proxy 127.0.0.1:8443
}
```

Keep the proxy's access log off. Clients fetch data through signed links that carry their signature in the query string, and the server never logs query strings.

## 7. Create the first tenant

A tenant is one customer of the cache: a team or a company, with its own quotas, caches and members.

```sh
docker exec -i oxabl-cache oxabl-cache-admin \
  --server http://127.0.0.1:9091 --token-file /dev/stdin \
  tenant create acme --hard-quota 20GiB < operator-token
```

```text
id            t_4h984yab6n0f7wmyd54988138g
name          acme
status        active
hard_bytes    21474836480
egress_bytes  -
rate          50
burst         200
created_at    1793742045
```

Then mint the tenant's first admin token, and hand it to the tenant through a secure channel:

```sh
docker exec -i oxabl-cache oxabl-cache-admin \
  --server http://127.0.0.1:9091 --token-file /dev/stdin \
  token create --tenant acme --expires 30d < operator-token > acme-admin-token
```

```text title="acme-admin-token"
id          a1tgvk22x5m2122d
expires_at  1796334045
token       oxc_<the secret, shown once>
```

Every command is in the [`oxabl-cache-admin` reference](/cache/operate/admin/).

## 8. Back up and stop

What matters in a backup is the metadata. The stored data can be rebuilt by CI, at the cost of a cold cache. `backup` copies the SQLite database while the server keeps running:

```sh
docker exec oxabl-cache oxabl-cache-server backup --to /var/lib/oxabl-cache/meta-backup.db
docker cp oxabl-cache:/var/lib/oxabl-cache/meta-backup.db ./meta-backup.db
```

```text
{"backup":"/var/lib/oxabl-cache/meta-backup.db"}
```

`backup` refuses to overwrite a file, so give each backup its own name, and copy it off the machine:

```text
oxabl-cache-server: bad request: /var/lib/oxabl-cache/meta-backup.db already exists
```

`docker stop oxabl-cache` sends `SIGTERM`. The server stops taking requests, finishes the ones in flight, and exits `0`. To upgrade, build the new image, remove the container and start it again with step 4: database changes run at start-up.

Source: https://docs.oxabl.org/cache/self-host/quick-start/index.mdx
