Quick start
You do not have to build Helsa to run it. Every push to main publishes container
images, and the Compose files use them by default; building from source is
the second path, and it stays fully supported.
| Image | |
|---|---|
| API, worker and the token CLI | ghcr.io/nordic-sys/helsa/backend |
| Web dashboard | ghcr.io/nordic-sys/helsa/web |
Two images, not three — the token CLI runs from the backend image rather than having one
of its own. Both are built for linux/amd64 and linux/arm64, so a Raspberry Pi, an ARM
VPS or an Apple Silicon machine runs them natively. The tags each image carries:
| Tag | What it is |
|---|---|
main |
the tip of the default branch — the default, and the newest thing that exists |
sha-… |
one exact build, and the only tag that never moves |
1.2.3, latest |
a release. ⚠️ There are none yet, so latest does not resolve — do not put it in your .env expecting it to work |
One backend image carries all three binaries; the Compose file picks which one each service runs.
1. Get the Compose files
Section titled “1. Get the Compose files”The images are the application, but the shape of a safe deployment — the Compose files, the Caddy configuration, the PKI recipe — lives in the repository. So you still start with a clone; you just do not compile anything.
git clone https://github.com/nordic-sys/helsa.gitcd helsa/deploycp .env.example .envLog in to the registry
Section titled “Log in to the registry”2. Configure secrets
Section titled “2. Configure secrets”.env holds every secret the stack needs. It is git-ignored, and it must stay
that way.
# Run these and paste the output into .env.openssl rand -base64 36 # POSTGRES_PASSWORDopenssl rand -base64 36 # REDIS_PASSWORDopenssl rand -base64 36 # RABBITMQ_PASSWORDopenssl rand -base64 48 # HELSA_JWT_SECRET| Variable | What it is | If you get it wrong |
|---|---|---|
POSTGRES_PASSWORD |
Database password. | The API cannot start. |
REDIS_PASSWORD |
Cache password. | Readiness check fails. |
RABBITMQ_PASSWORD |
Queue password. | Uploads are accepted but never processed. |
HELSA_JWT_SECRET |
Signs device tokens. | Anyone who knows it can mint a token for your data. Treat it like a private key. |
HELSA_PULL_POLICY |
always pulls the published images; build compiles from this checkout. |
With always and no registry login, nothing starts. |
Keep a copy of .env in a password manager. If you lose HELSA_JWT_SECRET you
have to reissue every device token; if it leaks, you have to rotate it, which also
invalidates every token.
3. Start the data services
Section titled “3. Start the data services”docker compose up -ddocker compose psWait until timescaledb, redis, and rabbitmq all report healthy. The first
start takes longer: TimescaleDB initialises its data directory.
None of these publish a port. You cannot reach the database from another machine, and that is the intended state.
4. Run migrations — deliberately
Section titled “4. Run migrations — deliberately”docker compose --profile tools run --rm migrate upVerify:
docker compose exec timescaledb \ psql -U helsa -d helsa -c '\dt'You should see users, devices, workouts, sleep_segments,
activity_summary, goals, sync_state, achievements, and the samples
hypertable.
5. Pull and start the application
Section titled “5. Pull and start the application”make prod-pull # docker compose -f docker-compose.yml -f docker-compose.prod.yml pullmake prod-up # ... up -dThat brings up the API, the worker, the web dashboard and the Caddy proxy. To check what you are actually running:
docker compose -f docker-compose.yml -f docker-compose.prod.yml imagesFor local development only
Section titled “For local development only”If you want the data services in Docker and the Go code on your machine (an edit
and make run-api cycle), skip the images entirely and use the dev overlay, which
publishes the data ports to 127.0.0.1:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -dcd ../backend && make migrate && make run-api # and make run-worker in a second shell6. Confirm it answers
Section titled “6. Confirm it answers”curl -s http://127.0.0.1:8080/healthz# {"status":"ok"}
curl -s http://127.0.0.1:8080/readyz# {"status":"ready"} — database, Redis, and the queue are all reachable/readyz is the one to trust: /healthz only proves the process is alive, while
/readyz proves it can talk to its dependencies.
Anything else needs a token:
curl -s -o /dev/null -w '%{http_code}\n' \ 'http://127.0.0.1:8080/v1/summary?range=day&metrics=stepCount'# 401A 401 here is a pass, not a failure. Next step: issue a device
token.
Building from source instead
Section titled “Building from source instead”Nothing about Helsa requires the published images. Building needs no registry login, is the only way to run a change you have made, and is what you should do if you would rather not trust a binary you did not compile.
make prod-build # HELSA_PULL_POLICY=build docker compose ... buildmake prod-up-source # ... up -d, from what you just builtOr set HELSA_PULL_POLICY=build in .env and forget about it — every make prod-* target then builds.
You need the same things either way (Docker and Compose v2), plus roughly two minutes of compilation on first build.
7. Stopping and starting
Section titled “7. Stopping and starting”make prod-down # stop, keep datadocker compose -f docker-compose.yml -f docker-compose.dev.yml downdocker compose -f docker-compose.yml -f docker-compose.dev.yml up -dTo wipe everything, including the database volume:
docker compose down -vKeeping it up to date
Section titled “Keeping it up to date”git pull # the Compose files and the migrationsmake prod-pull # the imagesmake prod-migrate # only if the pull brought new migrations — read them firstmake prod-upWhat this is not
Section titled “What this is not”This local stack is not a deployment. Without the proxy it has no TLS and no client certificate check, and a database whose only protection is that nothing is published beyond loopback. It is fine for trying things out and for development. Before you point a phone at it from outside your machine, work through Deployment.