Get started
A running instance, and a ticket in it.
Two things to configure, one wizard, and six keys. Everything else has a defensible default or is derived, and can be changed later from settings.
Run it against your own Postgres
One image — the front end, the API and a proxy that routes between them — on one origin. Kanso does not ship a database: a container that owns your data is a container you cannot restart without thinking about it.
docker run -d -p 80:80 -p 443:443 -v kanso-data:/data \
-e KANSO_PUBLIC_URL=https://kanso.example.com \
-e SPRING_DATASOURCE_URL='jdbc:postgresql://db.internal:5432/kanso?sslmode=require' \
-e SPRING_DATASOURCE_USERNAME=kanso -e SPRING_DATASOURCE_PASSWORD=… \
ghcr.io/tykok/kanso
Those two things — the URL it lives at and the database it writes to — are the whole configuration. Caddy obtains a certificate for that host on its own, for linux/amd64 and linux/arm64 alike.
No database yet? docker/docker-compose.yml ships a postgres:16-alpine beside the image. Copy it, change KANSO_PUBLIC_URL and the password, then docker compose up -d.
The host in SPRING_DATASOURCE_URL is resolved from inside the container. On Compose it is the service name; for a Postgres running on this machine but outside Docker it is host.docker.internal. It is never localhost, which is the container itself.
Or run it from source
The repository's own docker-compose.yml is a different thing: it builds both applications from source and serves them on two ports, which is what the Playwright suite drives. Fill in OIDC credentials in .env, or set KANSO_AUTH_MODE=dev.
cp .env.example .env
docker compose up
- localhost:3000
- The web application
- localhost:8080
- The REST API, the WebSocket and the MCP endpoint
- localhost:8080/actuator/health
- Liveness, readiness and the database check
To run the applications outside Docker and keep only Postgres in it:
docker compose up -d db # Postgres only
cd apps/api && ./gradlew bootRun # http://localhost:8080
cd apps/web && pnpm install && pnpm dev # http://localhost:3000
The setup wizard
The first launch opens a wizard rather than a login screen, because there is nobody to log in as yet. It has four steps and only the first is required.
- The owner account. An email address and a password. This account can configure the instance; nothing else can.
- Notion. Optional, and reopenable. Connecting it is a page of its own — see Notion.
- Google sign-in. Optional. An instance can run on passwords alone.
- Preferences. Theme, accent, sidebar. Per person, not per instance.
Every step after the account is skippable and reopenable from settings — , — so you can be filing tickets in under a minute and decide the rest later.
Whoever comes next arrives through an invitation link the owner generates, and sees only the preferences step. Notion and Google are instance-wide, so a member has nothing to decide there and the steps are hidden rather than shown disabled.
The first team, and the first ticket
A ticket belongs to a team, so a team comes first — the sidebar creates one. From then on the keyboard is the interface, and the same keys work in the list, on the board and on the timeline.
- c
- Create, from anywhere
- e
- Rename in place
- 1 … 6
- Your team's first six statuses, in its own order
- ⇧s
- Pick a status by name, including a seventh
- ⌘K
- The command palette (
Ctrl+Kelsewhere) - ?
- Every shortcut, as this account has them bound
The ticket gets a short identifier of its team's own — KAN-1, then KAN-2 — allocated inside the insert transaction from a counter on the team row. Two people pressing c at the same moment get 41 and 42: no gaps, no collisions.
Where to go next
- The model — teams, projects, tickets, statuses and the documents beside them.
- Working in Kanso — one query drawn three ways, filters, cycles, workload, triage.
- Notion — what is mirrored, what is not, and why Notion never wins.
- Agents and the API — the MCP door for an agent, tokens and webhooks for a service.
- Self-hosting — TLS, what
/dataholds, and what to back up.