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_MODE | The Hub | Public 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). |
coolify | A 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.
-
Copy
deploy/andhub/to the host. From the repository on your machine:BUGORO_HOST=<host> deploy/scripts/deploy.sh sync<host>is your host's ssh alias. WithoutBUGORO_HOST,syncreads it from the first line of~/.config/bugoro/host, and with neither it stops: there is no default. -
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:composeorcoolify. Withcoolify, the Hub's keys and its OAuth pair live in Coolify, not in.env.HUB_PUBLIC_HOST: the Hub's host name, such asqa.example.com. The Hub refuses to start without it, so every link it makes is public. DNS can come later.FARM_APK_HOSTSmust be set:<hub-host>,gitlab.com,<the host gitlab.com redirects package downloads to>. Find the last one withcurl -sIon 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 verifyup 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
Point
<hub-host>at the Hub: anArecord to the host's public address for Coolify, or the tunnel's public hostname forHUB_MODE=compose.-
Create the OAuth application in GitLab (User or Group settings > Applications):
- name Bugoro Hub, confidential;
- scopes
apiandread_user; - redirect URI
https://<hub-host>/auth/gitlab/callback.
Its ID and secret go in
GITLAB_OAUTH_CLIENT_IDandGITLAB_OAUTH_CLIENT_SECRET: in Coolify, or in the host's.envforHUB_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:
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.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.-
Environment Variables. Untick Available at Buildtime on every one, the four keys Coolify generates included, and copy those four keys to a password manager.
Variable Value HUB_PUBLIC_HOST<hub-host>, the domain's host, withouthttps://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. Advanced. Connect To Predefined Network, Preview Deployments, Allow Public PR Deployments, Inject Build Args and Drain Logs off; Force Https on; Auto Deploy off.
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 passesThen 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_projectThe 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-tokenIt is safe to run again. It sets up:
- the labels
bugoro,severity::*and the 13qa::<state>labels; - the webhook
https://<hub-host>/webhooks/gitlabfor 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 ciasks 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,uprefuses and leaves the CI daemon alone; the runner stops once its jobs end, and the nextup --with cistarts 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.upthen checks the rules again from outside, and the daemon's disk budget, and starts the runner only if both pass.verify --with cichecks 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 theapiscope, 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 backupstops 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/.envsomewhere 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(plainverifybefore 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