KESTREL docs
v1.0.0
Get help
● Start · Quick install on a VPS

Install with Docker
one command on an Ubuntu VPS.

The installer sets up Docker, writes your secrets, builds the server and the web app and starts them behind Caddy with automatic HTTPS. About 30 minutes, most of it waiting for the first build.

install.shdocker-compose.ymlCaddy HTTPS

01What gets installed

docker-compose.yml starts these containers:

ServiceWhat it isReachable
serverThe KESTREL server (server/Dockerfile, Node.js 22). Holds the database in the volume kestrel-data (/data) and writes backups to ./backups.Only inside Docker, port 8080
webCaddy serving the Flutter web app (app/Dockerfile.web) and proxying /api/*, /health and /admin to the server (deploy/Caddyfile). Gets and renews the HTTPS certificate.Ports 80 and 443
postgresPostgreSQL 16. Only when you choose it (compose profile postgres).Only inside Docker

02Point your domain to the server

At the company where you bought the domain, add an A record:

TypeNameValue
Atrade (or @ for the bare domain)your VPS IP address

Wait a few minutes, then check with ping trade.example.com: it must show your VPS IP. If you use Cloudflare, set the record to DNS only (grey cloud) for the install. See Cloudflare for the orange cloud.

03Put the files on the server

Unzip the package on your computer. Upload the kestrel folder (the one with install.sh in it) to the server, for example with scp, then connect with SSH:

scp -r kestrel root@203.0.113.7:/root/
ssh root@203.0.113.7

Use your own server address instead of 203.0.113.7. A file manager such as FileZilla or Cyberduck over SFTP works too.

04Run the installer

cd kestrel
sudo ./install.sh

The installer (install.sh) does this, in order:

  1. Checks the system. It is tested on Ubuntu 22.04 and 24.04 and warns, but continues, on other systems.
  2. Checks memory. With less than about 3 GB of RAM plus swap it offers to create a 2 GB swap file, because building the web app needs it.
  3. Installs Docker from Docker's official apt repository, if it is missing.
  4. Asks two questions: your domain and your email. The email is only given to the HTTPS certificate authority. Leave the domain empty to run on the IP over plain HTTP (testing only).
  5. Checks DNS. It warns if the domain does not point to this server yet, and asks whether to continue.
  6. Asks for the database on the first install: SQLite (default) or PostgreSQL.
  7. Writes .env (permission 600): a random KESTREL_MASTER_KEY, your domain, PUBLIC_URL, CORS_ORIGINS, TRUST_PROXY_HOPS=1, SERVER_PUBLIC_IP and, for PostgreSQL, a random database password. Secrets are never printed.
  8. Opens ports 80 and 443 in ufw, only if ufw is already active.
  9. Builds and starts everything with docker compose up -d --build and waits until the server is healthy. The first build takes 5 to 15 minutes.

At the end it prints your URL, the health result and the next steps.

Safe to run again

Running ./install.sh again keeps your .env and never changes a secret. It re-applies the stack.

Without questions

For scripts, give the answers as variables:

sudo DOMAIN=trade.example.com ADMIN_EMAIL=me@example.com USE_POSTGRES=n ./install.sh

05Back up .env right now

Do this before anything else

.env contains KESTREL_MASTER_KEY. Your users' exchange API keys are stored encrypted with it, and the operator console's secrets are encrypted with a key derived from it. If you lose it, every stored API key becomes unreadable and must be entered again. Copy .env into a password manager or onto an encrypted drive. Never email it and never commit it to Git.

cat .env    # shows the file on your screen only; copy it into your password manager

06First start

  1. Open the app. Go to https://your-domain and register your account.
  2. Get the console setup code. On the server run:
    docker compose logs server | grep -A3 "setup code"
    The code is printed once per start until the owner exists; any printed code works.
  3. Create the console owner. Open https://your-domain/admin, enter the code, your email and a password of at least 12 characters. See First owner.
  4. Note your server IP. Open https://your-domain/api/server-ip. It answers {"ip":"203.0.113.7"}. You whitelist this IP on every exchange key (Exchange API keys). The app shows it too, under Settings, Exchange accounts.

07Check that it runs

docker compose ps                       # server, web (and postgres) should be Up / healthy
curl -s https://your-domain/health      # {"ok":true,"version":"1.0.0","db":"ok","worker":{...}}
docker compose logs --tail 100 server   # the server log

worker.lastBeatMs in /health is the last heartbeat of the trading worker, in epoch milliseconds.

08PostgreSQL instead of SQLite

Answer y to "Use PostgreSQL instead?" on the first install (or pass USE_POSTGRES=y). The installer then sets COMPOSE_PROFILES=postgres, a random POSTGRES_PASSWORD and DATABASE_URL=postgres://kestrel:…@postgres:5432/kestrel in .env. The question is asked only when .env has no DATABASE_URL yet; switching later means moving the data yourself.

09Everyday commands

Run them in the kestrel folder.

CommandWhat it does
docker compose psWhat is running
docker compose logs -f serverFollow the server log (Ctrl+C to stop following)
docker compose restart serverRestart the server
docker compose up -dApply changes to .env and start what is stopped
./update.shBack up, rebuild and restart (Updating)
./backup.shBack up the database and .env (Backups)
docker compose downStop everything. Your data volumes stay.
Never add -v

docker compose down -v deletes the volumes, which means the database and the HTTPS certificates. Use it only if you really want to delete everything.