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.
01What gets installed
docker-compose.yml starts these containers:
| Service | What it is | Reachable |
|---|---|---|
server | The 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 |
web | Caddy 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 |
postgres | PostgreSQL 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:
| Type | Name | Value |
|---|---|---|
| A | trade (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:
- Checks the system. It is tested on Ubuntu 22.04 and 24.04 and warns, but continues, on other systems.
- 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.
- Installs Docker from Docker's official apt repository, if it is missing.
- 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).
- Checks DNS. It warns if the domain does not point to this server yet, and asks whether to continue.
- Asks for the database on the first install: SQLite (default) or PostgreSQL.
- Writes
.env(permission 600): a randomKESTREL_MASTER_KEY, your domain,PUBLIC_URL,CORS_ORIGINS,TRUST_PROXY_HOPS=1,SERVER_PUBLIC_IPand, for PostgreSQL, a random database password. Secrets are never printed. - Opens ports 80 and 443 in
ufw, only ifufwis already active. - Builds and starts everything with
docker compose up -d --buildand 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.
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
.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
- Open the app. Go to
https://your-domainand register your account. - Get the console setup code. On the server run:
The code is printed once per start until the owner exists; any printed code works.docker compose logs server | grep -A3 "setup code" - 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. - 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.
| Command | What it does |
|---|---|
docker compose ps | What is running |
docker compose logs -f server | Follow the server log (Ctrl+C to stop following) |
docker compose restart server | Restart the server |
docker compose up -d | Apply changes to .env and start what is stopped |
./update.sh | Back up, rebuild and restart (Updating) |
./backup.sh | Back up the database and .env (Backups) |
docker compose down | Stop everything. Your data volumes stay. |
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.