Self-host Bugoro

Takes about 1 to 2 hours

One Docker Compose project, bugoro, runs the emulator farm and an isolated CI runner on a Linux host with KVM. The Hub, the web app testers and GitLab talk to, runs on the same host or as a Coolify resource. Do everything on the host through deploy/scripts/deploy.sh; never run docker compose by hand.

Choose where the Hub runs

HUB_MODE in the host's .env decides it. The farm, the CI daemon and the runner stay in project bugoro either way.

HUB_MODEThe HubPublic route
compose (the default)Part of project bugoro: deploy.sh build-hub builds it on the host and up runs it, with its keys in the host's .env.A Cloudflare tunnel (up --with tunnel).
coolifyA Coolify resource that Coolify builds from your repository. You manage its domain, variables, deploys and logs in Coolify's UI.Coolify's proxy, on the resource's domain.

Only one Hub may run: both take 172.31.250.20 on the Bugoro network, where CI jobs and the farm reach it. deploy.sh refuses to start one while the other runs.

Prepare the host

Everything lives under /opt/bugoro, and every Docker object of project bugoro is named bugoro-*, so other services on the host are left alone.

  1. Copy deploy/ and hub/ to the host. From the repository on your machine:

    BUGORO_HOST=<host> deploy/scripts/deploy.sh sync

    <host> is your host's ssh alias. Without BUGORO_HOST, sync reads it from the first line of ~/.config/bugoro/host, and with neither it stops: there is no default.

  2. On the host, as root, take a baseline, create the secrets, set three values and run the check:

    cd /opt/bugoro/deploy/scripts
    bash host-preflight.sh             # the baseline: note the snapshot path
    bash deploy.sh init-secrets        # creates .env (prints names only)
    nano /opt/bugoro/deploy/.env       # HUB_MODE, HUB_PUBLIC_HOST, FARM_APK_HOSTS (below)
    bash deploy.sh build-hub           # HUB_MODE=compose only: Coolify builds its own Hub
    bash deploy.sh --check             # must print DEPLOY CHECK OK
  • HUB_MODE: compose or coolify. With coolify, the Hub's keys and its OAuth pair live in Coolify, not in .env.
  • HUB_PUBLIC_HOST: the Hub's host name, such as qa.example.com. The Hub refuses to start without it, so every link it makes is public. DNS can come later.
  • FARM_APK_HOSTS must be set: <hub-host>,gitlab.com,<the host gitlab.com redirects package downloads to>. Find the last one with curl -sI on a package URL. The farm's own traffic isn't filtered, so this list is what keeps its APK downloads to the Hub and GitLab.

The preflight checks KVM, memory, disk, load and the ports, and snapshots every other running container so you can prove none was restarted. --check runs it, requires cgroup v2, and verifies the rendered compose file: only bugoro-* services, limits on each, ports on 127.0.0.1 only, no Docker socket.

Start the farm

bash deploy.sh up
bash deploy.sh verify

up runs the check again and starts nothing if it fails. It starts the farm, and with HUB_MODE=compose the Hub too. verify checks health, limits and log rotation; with HUB_MODE=coolify it also checks the Coolify Hub's networks, limits and health.

DNS and the GitLab OAuth app

  1. Point <hub-host> at the Hub: an A record to the host's public address for Coolify, or the tunnel's public hostname for HUB_MODE=compose.

  2. Create the OAuth application in GitLab (User or Group settings > Applications):

    • name Bugoro Hub, confidential;
    • scopes api and read_user;
    • redirect URI https://<hub-host>/auth/gitlab/callback.

    Its ID and secret go in GITLAB_OAUTH_CLIENT_ID and GITLAB_OAUTH_CLIENT_SECRET: in Coolify, or in the host's .env for HUB_MODE=compose. Testers sign in with it, and the Hub files issues with their tokens.

Put the Hub online

With HUB_MODE=compose, up already runs the Hub. Put the OAuth pair and TUNNEL_TOKEN in the host's .env, then run bash deploy.sh up --with tunnel for its public route. With HUB_MODE=coolify, set up the resource:

  1. Resource. In a Coolify project, click + New, then Public Repository: your Bugoro repository, branch main, Build Pack Docker Compose, Base Directory /, Docker Compose Location /deploy/coolify/hub.yml.

  2. General. Domains for bugoro-hub: https://<hub-host>:3000 (3000 is the Hub's port inside the container; nothing is published on the host). Turn Escape special characters in labels? off, so the proxy's network pin works.

  3. Environment Variables. Untick Available at Buildtime on every one, the four keys Coolify generates included, and copy those four keys to a password manager.

    VariableValue
    HUB_PUBLIC_HOST<hub-host>, the domain's host, without https:// or :3000. Required.
    GITLAB_OAUTH_CLIENT_IDThe OAuth application's ID. Required.
    GITLAB_OAUTH_CLIENT_SECRETThe OAuth application's secret. Required, a secret.
    BUGORO_INCIDENT_TOKENOptional, a secret. See loop health below.
    FARM_URLOptional: http://172.31.250.10:8080.
    GITLAB_URLOnly if your GitLab isn't gitlab.com.
  4. Advanced. Connect To Predefined Network, Preview Deployments, Allow Public PR Deployments, Inject Build Args and Drain Logs off; Force Https on; Auto Deploy off.

  5. No deploy webhook. Deploy only by hand, after the preflight. Coolify 4.1.2 has no Watch Paths for a public repository, so Auto Deploy would rebuild the Hub on every push to main, each merged agent fix included, without the preflight or any limit, and take it down for 30 to 60 seconds each time.

Coolify shows the Hub's secrets. It keeps the resolved values in plain text in its database and generated files, and shows them in the resource's UI. Anyone who can view the resource in Coolify can read the Hub's keys, the OAuth secret and the incident token. Limit Coolify access to the owner.

Moving from a Hub on the host

If a Hub already runs on the host, the Coolify Hub starts fresh on its own empty volume; the old volume stays for a rollback. On the host, as root, in /opt/bugoro/deploy/scripts:

bash host-preflight.sh             # the baseline: note the snapshot path
bash deploy.sh backup              # the old Hub's data, while HUB_MODE is still compose
nano /opt/bugoro/deploy/.env       # HUB_MODE=coolify, HUB_PUBLIC_HOST=<hub-host>
docker stop bugoro-hub             # stop, don't remove: both Hubs take 172.31.250.20
# click Deploy in Coolify, wait for Running (healthy), then:
bash deploy.sh verify              # add --with ci once CI runs
docker rm bugoro-hub               # only once the new Hub passes

Then connect the project, set up GitLab and set the CI/CD variables again (below): the new Hub starts empty, and they write its webhook and CI URLs. To roll back before the old container is removed, stop the resource in Coolify, set HUB_MODE=compose in .env, and docker start bugoro-hub. docs/self-hosting.md has every step, check and rollback in full.

Connect your project

In the Hub's container (bugoro-hub, the name bash deploy.sh hub-name prints, or Coolify's Terminal), connect the GitLab project. APP_KEY is the key your app passes in BugoroConfig; the webhook secret is BUGORO_WEBHOOK_SECRET from the host's .env. Values you leave out are kept, and no secret is printed.

GITLAB_PROJECT_ID=<project-id> PROJECT_PATH=<your-group>/<your-project> APP_KEY=<app-key> \
  BUGORO_WEBHOOK_SECRET=<webhook-secret> bin/rails bugoro:connect_project

The public dashboard hides report titles, because a title starts with the tester's note. Add PUBLIC_REPORT_TITLES=true to show them, for a demo project.

Set up the GitLab project

A dry run first (it only reads), then the real run. The token (Maintainer, api scope) goes in on stdin and never touches the host's disk.

ssh <host> 'bash /opt/bugoro/deploy/scripts/deploy.sh gitlab-setup --project <your-group>/<your-project> --dry-run' < ~/.config/bugoro/gitlab-token
ssh <host> 'bash /opt/bugoro/deploy/scripts/deploy.sh gitlab-setup --project <your-group>/<your-project>' < ~/.config/bugoro/gitlab-token

It is safe to run again. It sets up:

  • the labels bugoro, severity::* and the 13 qa::<state> labels;
  • the webhook https://<hub-host>/webhooks/gitlab for issues, notes, merge requests and pipelines;
  • fork pipelines off;
  • a project runner tagged bugoro, locked to the project.

Start CI

bash deploy.sh up --with ci
bash deploy.sh verify --with ci
  • The runner drains first. up --with ci asks the runner to stop gracefully: it takes no new job and lets running ones finish, for up to 15 minutes. If jobs still run then, up refuses and leaves the CI daemon alone; the runner stops once its jobs end, and the next up --with ci starts it. No job is ever aborted unless you add --force-abort-jobs.
  • CI egress fails closed. Jobs run in their own Docker daemon, bugoro-dind. Its entrypoint applies the job egress rules and checks them before the daemon starts; if that fails, the daemon never starts and no job runs. up then checks the rules again from outside, and the daemon's disk budget, and starts the runner only if both pass. verify --with ci checks again and stops the runner if the rules are missing.
  • Jobs may reach the farm, the Hub on the Bugoro network and the internet. They can't reach the rest of the Bugoro network (the host included), the host's other Docker networks, link local addresses, the host's public network or the CI daemon's own API.
  • The host's firewall is never touched: the rules live in the CI daemon's own network namespace.
  • Once, before agents use CI, run verify --with ci --restart-drill. It pauses CI, restarts the daemon and checks the rules come back.

Without runner rights. If your GitLab role can't register a runner, skip this step and run CI on GitLab's hosted runners: run gitlab-setup with --steps labels,webhook and ci-vars with --hosted-runners, and set FARM_URL on the Hub. The jobs call the Hub at its public URL and reach the farm through the Hub's relay, /farm/v1/..., which checks their CI ID token and passes the farm's token on. You give up the job egress rules, the project access token (use a token, with an expiry date, of a separate account that owns nothing and belongs to no project or group) and branch protection; docs/self-hosting.md has the details.

Set the CI/CD variables

From your machine, with your GitLab token on line 1 and the project access token for BUGORO_GITLAB_TOKEN (Reporter, api) on line 2. FARM_TOKEN comes from the host's .env. Dry run first:

{ cat ~/.config/bugoro/gitlab-token; echo; cat ~/.config/bugoro/bugoro-gitlab-token; echo; } |
  ssh <host> 'bash /opt/bugoro/deploy/scripts/deploy.sh ci-vars --project <your-group>/<your-project> --dry-run'
{ cat ~/.config/bugoro/gitlab-token; echo; cat ~/.config/bugoro/bugoro-gitlab-token; echo; } |
  ssh <host> 'bash /opt/bugoro/deploy/scripts/deploy.sh ci-vars --project <your-group>/<your-project>'

It sets BUGORO_HUB_URL, BUGORO_HUB_API_URL (http://bugoro-hub:3000: CI egress drops the host's public address, so jobs call the Hub's API over the Bugoro network), FARM_TOKEN (scope bugoro-shots/*) and BUGORO_GITLAB_TOKEN (scope bugoro-*). The CI components page explains the scopes.

Loop health and incidents

Every 5 minutes the Hub checks the QA loop itself: whether CI jobs start, the farm, the agent flows and its own GitLab calls. The dashboard shows each one. Two optional variables in the Hub's environment turn on the parts that need them, and no page ever shows them:

  • BUGORO_INCIDENT_TOKEN: a project access token with the Developer role (never Maintainer) and the api scope, with an expiry date. With it, the Hub opens a GitLab incident when a signal is down twice in a row, and closes it when the signal recovers. Without it, there are no incidents and no CI jobs check.
  • FARM_URL: http://172.31.250.10:8080, where the Hub checks the farm's /healthz. Without it, the farm isn't checked.

Keep it running

  • bash deploy.sh backup stops the Hub for a few seconds, archives its data to /opt/bugoro/backups, and keeps the newest 7. It works in both modes.
  • Back up /opt/bugoro/deploy/.env somewhere else too, and with Coolify the four generated keys: without them the stored tester tokens can't be decrypted.
  • To redeploy the Hub in Coolify, run the preflight by hand first, then Redeploy, then verify --with ci (plain verify before CI runs). The Hub refuses to start without both halves of the OAuth pair (HUB_REQUIRE_OAUTH), so a missing value shows as a failed deploy, not a broken sign in. The Hub is down for 30 to 60 seconds, so pick a moment when no MR pipeline is finishing.
ssh <host> 'bash /opt/bugoro/deploy/scripts/host-preflight.sh'
# Redeploy in Coolify, then:
ssh <host> 'bash /opt/bugoro/deploy/scripts/deploy.sh verify --with ci'

Prune CI leftovers daily, inside the CI daemon only. In /etc/cron.d/bugoro-ci-gc:

17 4 * * * root bash /opt/bugoro/deploy/scripts/deploy.sh ci-gc 2>&1 | logger -t bugoro-ci-gc