Deploy the server with Docker Compose
This is the single-host deployment: bugb-server and the Postgres that holds its state, brought up together by Compose. At the end you have a server answering on loopback and one bearer token for a developer to use.
For a cluster, see Deploy the server with Helm instead.
Before you start
Section titled “Before you start”You need three things from Bugb, and the first two arrive together with your licence:
- A registry credential for
ghcr.io/bugb-technologies/bugb-server. The image is published to a private repository and this is what pulls it. - A deployment key (
bugb_dk_live_…), issued at account.bugb.io, or a pinned licence token for a deployment that cannot reach the portal. - The deployment bundle, the
deploy/directory carryingdocker-compose.yml,.env.exampleandverify-image.sh.
And on the host: Docker 23 or newer with Compose v2. Check with
docker compose version.
Roughly 1 vCPU and 512 MB covers the server. Size Postgres to your finding volume, which is small: one row per verdict event.
Stand it up
Section titled “Stand it up”-
Log in and pull the image.
Terminal window echo "$BUGB_REGISTRY_TOKEN" | docker login ghcr.io -u "$BUGB_REGISTRY_USER" --password-stdindocker pull ghcr.io/bugb-technologies/bugb-server:0.1.0Pin a version tag. There is deliberately no
latest: a customer on a moving tag gets a different server, and therefore a different verdict lattice, on their next pull, chosen by nobody.Tag What it is 0.1.0(X.Y.Z)a release, written once and never rewritten. Use this one sha-abc1234one specific build of main, immutable forever@sha256:…the digest, the only reference that cannot be repointed at all Then check what you pulled, before it goes anywhere near a client:
Terminal window ./deploy/verify-image.sh ghcr.io/bugb-technologies/bugb-server:0.1.0It brings its own throwaway SQLite database, so it needs nothing but Docker and the image. It asserts that the engine and the dashboard are in the image, that the build channel really is
releaseand staysreleaseeven withBUGB_BUILD_CHANNEL=devset, and that a missing, malformed or foreign licence produces402everywhere with its own message. Its last five checks need a licence of their own, supplied in$BUGB_VERIFY_LICENSE; without one it still runs the gating checks and warns that the serving ones were skipped.If the runtime host cannot reach ghcr.io, pull somewhere that can, push into a registry it mirrors, and set
BUGB_SERVER_IMAGEin the next step to that name. -
Write the environment file.
Terminal window cp deploy/.env.example deploy/.env$EDITOR deploy/.envCompose reads
deploy/.envon its own, because it sits beside the compose file. It is gitignored, it holds a database password, and it belongs on the deployment host and nowhere else.Four values need changing before this is safe to run:
POSTGRES_PASSWORD=<openssl rand -base64 24>BUGB_SERVER_DSN=postgresql://bugb:<that same password>@postgres:5432/bugbBUGB_SERVER_BIND=127.0.0.1BUGB_DEPLOYMENT_KEY=bugb_dk_live_xxxxxxxxBUGB_DEPLOYMENT_KEYis the licence, and without it this deployment serves nothing. Set the deployment key orBUGB_LICENSE_TOKEN, never both. See License a deployment.BUGB_SERVER_IMAGEandBUGB_SERVER_TAGdecide what Compose runs. They default toghcr.io/bugb-technologies/bugb-serverand0.1.0, so leave them alone unless you mirrored the image somewhere else.The password is spelled twice and both copies must match. Postgres wants three variables and the server wants one DSN, and nothing reconciles them. A mismatch shows up as the server restarting in a loop with
password authentication failed. If the password contains@ : / ? #or%, percent-encode it in the DSN, so@becomes%40.postgresin that DSN is the Compose service name, which resolves on the Compose network. The port is5432inside the container whatever you publish on the host. -
Bring both services up.
Terminal window docker compose -f deploy/docker-compose.yml pulldocker compose -f deploy/docker-compose.yml up -dpullthenupis the normal path: the image is published and there is nothing to build.The server waits for Postgres to report healthy before it starts. It applies its own schema during startup, so a server that starts against a database not yet accepting connections exits rather than retrying.
-
Check both are healthy.
Terminal window docker compose -f deploy/docker-compose.yml psNAME STATUSbugb-postgres-1 Up 25 seconds (healthy)bugb-server-1 Up 25 seconds (healthy)Allow up to about 30 seconds. The healthcheck has a 30 second start period because the startup schema step is the slowest thing it does against a cold database.
Prove it is actually working
Section titled “Prove it is actually working”There is no /health route. The shipped healthcheck reads the two routes that
do exist, and the second one is the interesting one:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/openapi.jsoncurl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/v1/repos200401A 401 on the second is the good answer. It means a request reached the
auth check, which opens a database connection before it refuses you. If Postgres
is unreachable that same request answers 500, and the container is marked
unhealthy, which is correct: a server that can serve its schema but not its
database is not healthy in any sense that matters.
A 402 on the second means the deployment holds no licence it accepts.
docker compose logs server | head carries the sentence saying which of the
four failures it is: no licence, a mangled token, a licence Bugb did not sign,
or a licence for another product.
Run the same check by hand at any time:
docker compose -f deploy/docker-compose.yml exec server /usr/local/bin/bugb-healthcheckhealthy: serving on http://127.0.0.1:8000, database reachablePut TLS in front of it
Section titled “Put TLS in front of it”BUGB_SERVER_BIND defaults to 127.0.0.1 on purpose. Bearer tokens travel over
this port in clear text.
Terminate TLS in front of it with nginx, Caddy, or your load balancer. Widen the
bind to 0.0.0.0 only when that terminator is on another host, and only
deliberately.
Mint the first token
Section titled “Mint the first token”Every request carries a bearer token, and the token is what determines the tenant. A caller cannot assert its own tenancy in a request body, which is what makes the isolation hold.
docker compose -f deploy/docker-compose.yml exec server \ python scripts/mint_token.py --tenant acme --identity alice --role memberminted member token for alice@acme — shown once, not storedbugb_ZRbsvgLqZdCncXcvGxvUOIEG3-KHjZMcy9i3YjdfUtUThe confirmation goes to stderr and the token to stdout, so $(...) captures
only the token. Only its sha256 is stored, so there is no way to show it
again. If it is lost, mint another.
Minting is a write, so it is licence-gated like every other write: on a deployment holding no licence this script refuses too, rather than handing out a credential nothing will accept. A seat is a distinct identity among a tenant’s non-revoked tokens, and the cap comes from the licence.
Roles run viewer < member < security_lead < admin. A developer or a CI
job needs member, because viewer cannot push:
{"detail":"role 'viewer' cannot push events; member or above is required"}HTTP 403Mint one token per identity, and a separate one per CI pipeline, so revoking one
does not revoke everyone. scripts/revoke_token.py takes --token-hash,
--token, or --identity to void every token a principal holds.
Hand the token to a developer along with the server URL, and send them to Connect the CLI to a server.
Back it up
Section titled “Back it up”The named volume bugb-pgdata is the state of record. The image is disposable;
the volume is not.
docker compose -f deploy/docker-compose.yml exec -T postgres \ pg_dump -U bugb -d bugb --format=custom > "bugb-$(date +%F).dump"--format=custom restores with pg_restore and can be restored selectively.
Take one before every upgrade, and schedule one alongside whatever else you back
up. Restore a dump somewhere disposable at least once: an untested backup is a
hypothesis.
An upgrade is: back up, load the new image, recreate. The schema step is idempotent and runs on every boot, so there is no separate migration to run. Migrations are forward only, which means a backup is the only real rollback for a release that changes the schema.
Related
Section titled “Related”- License a deployment: the deployment
key, where it goes in
deploy/.env, and every state a deployment can land in. - Deploy the server with Helm: the same product on a cluster.
- Connect the CLI to a server: the developer side.

