Docker
Run keelwave with the single self-contained image or Docker Compose.
The fastest way to a running keelwave is the app Compose profile. It starts
TimescaleDB, applies migrations, then starts the API and dashboard on
http://localhost:8080.
# from core/
docker compose --profile app up --buildThat brings up three services, in order:
| Service | Image | Role |
|---|---|---|
db | timescale/timescaledb:latest-pg18 | Postgres + TimescaleDB, port 5432 |
migrate | migrate/migrate:v4.19.1 | Applies cmd/migrate/migrations, then exits |
app | ghcr.io/keelwave/keelwave:local | API + dashboard, port 8080 |
migrate waits on the db healthcheck (pg_isready), and app waits for
migrate to complete successfully. Database files persist in the keelwave_db
volume.
Check that it is up:
curl http://localhost:8080/v1/health
# {"data":{"status":"ok","version":"...","env":"development"}}Then open http://localhost:8080 for the dashboard and
http://localhost:8080/v1/docs for the API reference.
What the image contains
The keelwave image is self-contained: one binary, one process, one port, serving both the Go API and the React dashboard.
The Dockerfile builds in three stages:
- web (
node:22-bookworm-slim) —pnpm buildcompiles the dashboard as a static client-only SPA and copies the client bundle intointernal/web/dist. - build (
golang:1.26-bookworm) — overlays that directory, thenCGO_ENABLED=0 go build -trimpathproduces/out/keelwavewith the dashboard embedded via//go:embed. Cross-compiled usingTARGETOSandTARGETARCH. - runtime (
gcr.io/distroless/static:nonroot) — non-root, no shell,EXPOSE 8080,ENTRYPOINT ["/usr/local/bin/keelwave"].
Routing inside the binary: chi matches /v1/* first (API, /v1/health,
/v1/docs, /v1/openapi.json), and a catch-all mounted last serves the
embedded dashboard. Unknown extensionless paths fall back to index.html so
client-side routes deep-link; a missing asset returns 404.
Because the dashboard is same-origin with the API, it calls /v1/* over
relative paths — no separate dashboard container, and no CORS setup needed for
the dashboard itself.
The image does not run migrations on start. Apply them out-of-band — via
the migrate service above, or make migrate-up — before rolling a new image.
Running the image directly
The binary needs a reachable Postgres/TimescaleDB. Minimum:
docker run --rm -p 8080:8080 \
-e DB_ADDR="postgres://keelwave:keelwave@host:5432/keelwave?sslmode=disable" \
ghcr.io/keelwave/keelwave:latestTo build the image locally instead of pulling it:
docker build --build-arg VERSION="$(git describe --tags --always)" -t keelwave:dev .Released images are multi-arch (amd64 + arm64) and published to
ghcr.io/keelwave/keelwave on a v* tag, with provenance and SBOM. Tags
follow semver (X.Y.Z, X.Y, X), plus sha-<sha> and latest.
Database only
If you are running the Go binary from source, start just the database:
docker compose up -d db # TimescaleDB (pg18) on :5432
make migrate-up # apply all migrations
make run # API on :8080 (make dev for hot reload)On a fresh checkout go build embeds only a placeholder dashboard. Run
make web-build (or cd web && pnpm build) first if you want the real
dashboard in a locally built binary. The Docker web stage does this
automatically.
First API key
The server never stores plaintext API keys — only their SHA-256 hash — so a key is shown exactly once, at creation. There are two ways to get your first one.
Option 1 — seed script
make seed creates a user, an organization, a project, and one API key, then
prints the plaintext to stdout:
make seedproject_id: 0199...
project_name: dev
api_key_id: 0199...
api_key: kw_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
store the api_key now — server only keeps the SHA-256 hashCopy the kw_… value — that is what the SDKs authenticate with. Project and
key names default to dev and seed; override them with SEED_PROJECT_NAME
and SEED_KEY_NAME.
The seeded user is created without a password and is not email-verified, so
it cannot sign in to the dashboard. make seed exists to get an ingest key
quickly in development; use the registration flow below for dashboard access.
Option 2 — register, then create a key
Register a user. POST /v1/auth/register creates the account and sends a
verification email (needs RESEND_API_KEY and MAIL_FROM). Alternatively use
Google or GitHub OAuth via GET /v1/auth/oauth/{provider}/start, which
requires GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET or GITHUB_CLIENT_ID /
GITHUB_CLIENT_SECRET.
Verify the email. The link points at DASHBOARD_URL +
/verify-email/<token> and resolves through
GET /v1/auth/verify-email/{token}. Admin and project query routes reject
unverified users with 403.
Create an organization and a project with POST /v1/admin/orgs and
POST /v1/admin/orgs/{orgID}/projects, or through the dashboard.
Create the key with POST /v1/admin/orgs/{orgID}/projects/{projectID}/keys
and a {"name": "..."} body. The 201 response carries a key field holding
the plaintext kw_… value. It is returned once and never again.
SDKs send the key as Authorization: Bearer kw_<token> on /v1/ingest/*
requests.
Next
- Configuration — every environment variable the server reads.
- Production — upgrades, backups, TLS.