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:
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:
cp deploy/container/server.example.toml 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:
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 proxy3. 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:
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-tokencreated 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 1825278042The token went to operator-token; the messages above go to the terminal. Move the token to your secret manager. A second init refuses:
oxabl-cache-server: an operator token already exists; init mints the first one only4. Start the server
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-serverAll 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:
docker logs oxabl-cache{"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
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:
docker exec -i oxabl-cache oxabl-cache-admin \
--server http://127.0.0.1:9091 --token-file /dev/stdin health < operator-tokenmetadata ok
store ok
gc_leader true6. Put TLS in front
The server has no built-in certificate renewal. Put a proxy in front that has it, such as Caddy. This Caddyfile gets a certificate for the name and forwards to the server:
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.
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-tokenid t_4h984yab6n0f7wmyd54988138g
name acme
status active
hard_bytes 21474836480
egress_bytes -
rate 50
burst 200
created_at 1793742045Then mint the tenant’s first admin token, and hand it to the tenant through a secure channel:
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-tokenid a1tgvk22x5m2122d
expires_at 1796334045
token oxc_<the secret, shown once>Every command is in the oxabl-cache-admin reference.
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:
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{"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:
oxabl-cache-server: bad request: /var/lib/oxabl-cache/meta-backup.db already existsdocker 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.