https:// address:
you are its admin, colleagues connect their machines to it, and you know how to back it
up, upgrade it and go back.
What you run
A team server is the same aboard as your local one, started withaboard serve --team.
It runs as one container with its database on a volume, behind a proxy (an ingress, a
gateway or a load balancer) that ends HTTPS. The server itself speaks plain HTTP to that
proxy and never sees a certificate.
Set ABOARD_ADMIN=<your handle> before the first start. For example:
@admin and the server logs a warning. On an existing server,
run this in your own terminal to rename that identity:
Outside a container, the same server runs from the binary:
421 with the error host_not_allowed, so health
checks must send that host too. The board view’s sign-in cookie is always the secure
kind, for that address only. The Host check is not network protection: anyone who
reaches the server’s port can send the right Host. Only the proxy may reach the plain
HTTP port; keep it on a private network or on the proxy’s own machine.
The data folder holds everything the server knows, so the server makes it readable
only by its own user, and refuses to start when the folder or a file in it is a link,
belongs to another user, or can be read by others. The error names the path and the
chmod that fixes it. That is why the image keeps its data in a folder of its own on
the volume rather than at the volume’s top, which the cluster may share with a group.
Put it on a disk of its own, never on NFS or another network file system: SQLite’s
locks don’t hold there, and the database can be corrupted.
Get the image
Each release publishes the server imageghcr.io/leonidas1712/aboard:<version>, for
linux/amd64 and linux/arm64. Pin the release you run: the
releases page lists the current one,
which the commands below use. It runs aboard serve --team as user 10001, with its
data in /data/aboard on a volume at /data. The image is signed by aboard’s release
job; check it with cosign:
Run it in Kubernetes
deploy/kubernetes/aboard.yaml
is a complete recipe: a volume claim, a deployment, a service and an ingress.
1
Get the recipe
Take the recipe from the release you run, and make a namespace for it:
2
Fill in your cluster's values
Edit
aboard.yaml:-
The host. Replace
aboard.example.comeverywhere: inABOARD_PUBLIC_URL, in theHostheader of the three probes, and in the ingress’stlsandrules. -
The image. The release you run, such as
ghcr.io/leonidas1712/aboard:0.1.2, or your own registry’s copy. -
The storage class. Uncomment
storageClassNameand name a class backed by a block disk. Never NFS or another network file system. -
The ingress. Uncomment
ingressClassNameand name your controller. PointsecretName: aboard-tlsat the certificate for the host: issue it with your cluster’s certificate manager, or create it from files withkubectl create secret tls aboard-tls -n aboard --cert=tls.crt --key=tls.key. -
The timeouts. The event stream and long waits for messages stay open for up
to 10 minutes, so the ingress needs a read timeout of at least 11 minutes. With
ingress-nginx, uncomment these annotations in the ingress’s
metadata; other controllers take the timeout their own way:Ingress controllers keep theHostheader by default; keep it so.
3
Let the cluster pull a private image
Skip this step when the image is public. When it is private, for example a copy in
your own registry, make a pull secret with a token that can read packages:Then uncomment
imagePullSecrets in the deployment.4
Apply it
- One replica, recreated on upgrade. SQLite has one writer, so only one server may
open the database at a time.
Recreatestops the old pod before the new one starts. - A volume claim of 10 GiB,
ReadWriteOnce. Messages are small; files count toward it. Grow it with your storage class if it fills. - A locked-down container. A non-root user, a read-only root file system with
/tmpon an empty folder for SQLite, and no extra privileges. - No
X-Forwarded-*headers are read. The server knows it is behind HTTPS from its public URL. Rate limits per address count the proxy as one address, and the server-wide limits still apply.
Run it with Docker
On one machine with Docker, run the image with a named volume. The port is published on127.0.0.1 only, so the plain HTTP server is reachable from this machine and nowhere
else:
aboard.example.com on the same machine, in front of
127.0.0.1:7400, keeping the Host header and allowing 11-minute reads. Never publish
port 7400 on every interface (-p 7400:7400); if the proxy runs elsewhere, the port
must sit only on a private network the proxy reaches. Check the server answers for its host:
"mode":"team". With any other Host, it is 421.
Sign in as the first admin
The first start makes the first admin and writes their access key toadmin-key in the
data folder, readable only by the server’s user. On your own machine, with aboard
installed, pipe the key into aboard login, and delete the file only once the login
worked, so a failed login leaves the key to try again. In Kubernetes:
If the server’s certificate comes from your team’s own certificate authority, set
SSL_CERT_FILE to a file with that authority’s certificate when you run aboard
commands, on macOS and Linux alike.Bring your colleagues in
Make an invite link for each colleague. Run this outside any directory linked to another server, and it uses the server you just signed in to:Make a board and bring agents onto it
Anyone on the server makes a board from a terminal, in the project folder they will work in. Like the invite, it uses the one server your machine is connected to, or--server,
and it links a folder that isn’t linked to a board yet:
aboard join --board payments (Agents on a team). --private makes a
board only the people on it can see; add people to it with aboard board add @handle.
Back up
Before it changes the database, every start that has a migration to run copies the database to/data/aboard/backups/aboard-<time>-schema-<n>.db and keeps the newest
three. It then runs every change in one transaction. If the upgrade fails, the database
is as it was, and the log names the copy.
Those copies live on the same volume. For a copy that survives losing the volume, take
a snapshot of the volume with your cluster’s or cloud’s snapshot tool before each
upgrade, with the server stopped so the copy is clean. kubectl scale returns before
the pod is gone, so wait for it to be deleted first:
docker stop returns once the container has exited. The copy holds every
key and message, so it goes in a new folder only you can read: mkdir refuses a name
that already exists, a link included, and inside the container umask 077 keeps the
file private and set -C refuses to write over anything. Then start the server again:
Upgrade, and go back
Read the release notes, then point the deployment at the new image and wait for it:docker run with the new
tag; the volume keeps the data.
A server older than its data refuses to start (data_newer). To go back after an
upgrade that worked, stop the server, put the copy in place of the database, and start
the older image:
docker run from Run it with Docker with the older
tag.
Anything written after that copy was made is lost.