~ $ man chasen
How Chasen works.
All of it, on one page: what runs where, what a deploy does step by step, where your data goes, and how the two programs talk. Nothing here is hidden behind an account.
[01] the architecture
Everything runs on your server. The bucket holds the copy.
One box runs the proxy, the Chasen API, and your apps. Each app is one container with its SQLite files next to it. Two things are outside the box, and both are yours: the registry that holds your images and the bucket that holds your backups.
- 1your computerchasen deploy says which commit to deploy, and sends chasen.yml and the secrets over HTTPS, with a token. No SSH.
- 2your registryghcr.io or Docker Hub. Your computer or your CI pushes the image of each commit there. Nothing builds on the server.
- 3proxykamal-proxy, with Let's Encrypt certificates. It sends each domain to its container.
- 4chasen-serverThe API. It pulls your image, asks /up, swaps the version, and backs up every database.
- 5your appsOne container each, with its SQLite files in /storage. Visitors reach them through the proxy.
- 6your bucketEach hour a checked snapshot, each second the new changes. A new server gets its data back from here.
[02] a deploy
What chasen deploy does, step by step.
To the server, a deploy is one HTTPS request. The server does the work and streams its output back, so you see each step as it runs. If your connection drops, the deploy still runs to its end.
- 1readsThe CLI reads chasen.yml and gets the values of your secrets, from your secret manager or from the environment. A missing secret stops here.
- 2buildsIt builds the image of the current git commit with your Docker, and pushes it to your registry: ghcr.io or Docker Hub.
- 3sendsIt sends one request to the API of your server, over HTTPS: the settings, the secrets, and the name of the image. There is no SSH.
- 4pullsThe server pulls that image from your registry: ghcr.io or Docker Hub, private or public. Nothing builds on the server.
- 5backs upIt copies every SQLite database of the app and checks each copy. A failed backup stops the deploy.
- 6startsIt starts the new container next to the old one and asks /up once a second.
- 7swapsOn the first 200, the proxy moves the traffic to the new container. The old one gets SIGTERM and 10 seconds to finish.
- 8or notNo 200 in 30 seconds: the server removes the new container. The old version never lost the traffic.
[03] the data
Your data is SQLite. That is the point.
A SQLite database is a file on the disk of the server. There is no database server to install, tune, upgrade, or pay for. A query never leaves the machine, so it is fast. A backup is a copy of a file.
One server with SQLite is only safe with backups you can restore, so Chasen keeps two kinds. Each hour and before each deploy, it makes a consistent copy of every database, runs an integrity check on the copy, and sends it to your bucket. Between two copies, Litestream sends the new changes to the bucket once a second.
- Kept: the newest copy of each of the last 24 hours, 7 days, 4 weeks, and 6 months.
- A restore deletes nothing. It moves the current databases aside first.
- The server can die. Set up a new one with the same bucket and run
chasen deploy. The data comes back first, about one second old. - Not SQLite? Uploaded files persist across deploys, but they have no backup yet. An app can use a Postgres that runs elsewhere.
[04] the protocol
One request for each command.
The API is small enough to call with curl. The command is in the path, the arguments are in the query, the request body is the input, and the response is the output as it happens. The last line is the exit code.
$ curl -X POST "https://api.example.com/v1/status?arg=shop" \ -H "Authorization: Bearer $CHASEN_TOKEN" App: shop Version: 3f9a2c1 State: Up 2 hours URL: https://shop.example.com Backup: 20261001T120000Z Replica: live
- The login is the OAuth 2.0 device flow. You approve it in a browser with the token of the server, and the CLI gets a token of its own.
- One owner for each server. Every login to a server can deploy every app on it.
- In CI, set
CHASEN_URLandCHASEN_TOKEN. There is no browser step.
[05] the parts
Boring, proven parts. Chasen is the small tool that holds them.
The server needs Docker and nothing else. There is no agent to keep alive, no control plane, and no hidden state: every file is in the list below.
| Docker | runs each app in one container |
| your registry | ghcr.io or Docker Hub holds the image of each commit. Your computer or your CI builds it |
| kamal-proxy | HTTPS, routing by domain, and the swap with no downtime. From 37signals' Kamal |
| Let's Encrypt | the certificates, one for each domain, renewed by the proxy |
| SQLite | your data, and the small database of the server itself |
| Litestream | streams each database to your bucket. It runs inside chasen-server |
| matcha | the deploy engine: it holds the proxy and the containers together |
| Go | chasen and chasen-server are one static binary each |
/usr/local/bin/chasen-server the binary. The API container runs it /etc/chasen/config.yml the base domain, the server token, the bucket /etc/chasen/server.sqlite3 logins, and the history of every deploy /etc/chasen/env/<app>.json the env and the secrets of each app /var/matcha/<app>/storage/ the storage of the app: /storage in the container /var/matcha/<app>/backups/<time>/ the snapshots
[06] install
Seven lines, and the first app is live.
You need a server with Ubuntu or Debian, root access, and ports 80 and 443 open. Point a wildcard DNS record, *.example.com, to it first. Each app then gets its own name under that domain, with a certificate. On your computer you need Docker, git, and an account at ghcr.io or Docker Hub. The step-by-step guide →
$ curl -fsSL https://chasenhq.com/server | sh $ chasen-server setup --domain example.com $ chasen-server bucket --endpoint https://... --name backups --access-key-id ...
$ curl -fsSL https://chasenhq.com/cli | sh $ chasen add server example.com # opens a page. Type the token that setup printed on the server. $ docker login ghcr.io # once. The image goes to ghcr.io/you/shop, the name of your git origin on GitHub. $ chasen deploy Deployed shop 3f9a2c1 https://shop.example.com
- Both install scripts download one binary and check its checksum. To update the server, run both of its lines again: the install and
chasen-server setup. setupdoes not harden the server.chasen-server checkreports what is missing: SSH with keys only, a firewall, and security updates.- Your app is an image in a registry.
chasen deploybuilds it where you run it and pushes it. Put the same command in GitHub Actions, andgit pushis the deploy. - Your app follows a short standard: an image, one port,
/up, and its data in the storage.chasen checktests an app against it before it gets traffic.
[07] and the cloud?
The cloud is the same programs, on a server we set up.
The cloud adds one program, chasen-cloud. It speaks the same protocol as a server, creates the Hetzner server for you, and passes each command on to it. The server it creates runs the same chasen-server you can install yourself. So you can start on either side and change later: your apps are an image and a bucket.
| self-hosted | cloud | |
|---|---|---|
| the programs | chasen, chasen-server | the same two, plus chasen-cloud in front |
| the server | yours, from any provider | a Hetzner server that we create, in Europe or the USA |
| setup | you, with one command | done at the first deploy |
| hardening | you. chasen-server check lists what is missing | done at the first deploy |
| DNS | you: one wildcard record | done |
| the backup bucket | yours, one command | done |
| log in | chasen add server <domain> | chasen login |
| price | €0 | €9 a month, plus the server |
That is all of it. One server is plenty for the rest of us.