This testbed spins up a complete cvmfs-prepub stack using Docker Compose, exercising the full Option B path with two Stratum 1 receivers. It uses dev: true mode to avoid TLS and key management overhead, making it ideal for testing and development.
All persistent state lives under TESTBED_ROOT on the host (a single external disk), and all binaries under test are injected from SOFTWARE_ROOT without being baked into images. This allows you to swap binary versions without rebuilding containers.
The system simulates the complete CVMFS publishing workflow:
- Publisher submits a tar job to the cvmfs-prepub REST API
- cvmfs-prepub processes the tar, writes objects to the CAS (shared directory)
- cvmfs-prepub pre-warms two Stratum 1 receivers via HTTP announce and object PUT
- cvmfs-prepub commits the catalog update to cvmfs_gateway
- cvmfs_gateway updates
.cvmfspublishedin the repository root - Apache (Stratum 0) serves the repository over HTTP
- The CVMFS client container mounts the repository via FUSE and confirms file visibility
Two control-plane modes are available. The default mode uses direct HTTP announces between cvmfs-prepub and the Stratum 1 receivers. Activating docker-compose.mqtt.yml instead routes all control-plane signalling through a Mosquitto MQTT broker, exercising the Option B MQTT path described in REFERENCE.md §20.11.
Monitoring is built in with VictoriaMetrics, vmagent, and Grafana, providing real-time visibility into all services.
╔══════════════════════════════ Physical Host ═════════════════════════════════╗
║ ║
║ Browser ║
║ │ http://testbed.localhost:3000/bits-project/testbed/ ║
║ │ (bits-console SPA, served by Gitea Pages gh-pages branch) ║
║ │ ║
║ │ POST /api/v1/repos/testbed/bits-project/actions/workflows/ ║
║ │ bits-publish.yaml/dispatches ║
║ ▼ ║
║ ┌──────────────────────────────────────────────────────────────────────┐ ║
║ │ Docker Compose │ ║
║ │ │ ║
║ │ gitea:3000 ────────── dispatches workflow ──────────────────────► │ ║
║ │ │ act_runner (host) │ ║
║ │ (Pages at testbed.localhost:3000) │ │ ║
║ │ ┌─────────┴──────────┐│ ║
║ │ seeder ──init──► gitea (org/repo/pages) │ job steps ││ ║
║ │ │ • checkout ││ ║
║ │ cvmfs-prepub:8080 ◄── bits publish ──────────┤ • bits build ││ ║
║ │ │ │ • bits publish ││ ║
║ │ ├──► stratum1-a:9100/9101 └────────────────────┘│ ║
║ │ ├──► stratum1-b:9100/9101 │ ║
║ │ └──► gateway:4929 │ ║
║ │ │ │ ║
║ │ stratum0:80 │ ║
║ │ │ │ ║
║ │ cvmfs-client (FUSE) │ ║
║ │ │ ║
║ │ victoriametrics + vmagent + grafana:3001 │ ║
║ └──────────────────────────────────────────────────────────────────────┘ ║
╚══════════════════════════════════════════════════════════════════════════════╝
Step Who Action
──── ──────────── ──────────────────────────────────────────────────────────
1 Browser User fills in Package/Version/Platform form in bits-console
SPA (served via Gitea Pages at testbed.localhost:3000)
2 bits-console POST /api/v1/repos/{org}/{repo}/actions/workflows/
bits-publish.yaml/dispatches
with inputs: COMMUNITY, PACKAGE, VERSION, PLATFORM …
3 Gitea Enqueues a workflow_dispatch run; assigns run_id
4 act_runner Polls Gitea for pending jobs; picks up the run
(runner is registered with labels [self-hosted, bits])
5 act_runner Executes bits-publish.yaml steps on the physical host:
a) actions/checkout@v3 — checks out the repo into a workspace
b) Validate community config (reads ui-config.yaml)
c) Authorize submitter (checks admins list)
d) Set up bits environment (sources config/bits-setup.sh)
e) bits build — builds the package into $JOB_SCRATCH
f) bits publish — POSTs tar to cvmfs-prepub:8080
6 cvmfs-prepub Accepts tar, deduplicates objects, writes WAL entry
7 cvmfs-prepub Announces job to stratum1-a and stratum1-b (HTTP or MQTT)
Receivers pull objects from CAS; reply with "ready"
8 cvmfs-prepub Acquires lease from gateway:4929 (HMAC auth)
Updates catalog, commits → .cvmfspublished written
9 act_runner bits publish returns 0; step g) updates cvmfs-status.json
in the repo (git push to main)
10 Browser bits-console polls workflow run status via Gitea API;
shows "published" badge; user can verify via cvmfs-client
publisher ──POST /api/v1/jobs──► cvmfs-prepub:8080
│
┌─────────────┼─────────────┐
│ │ │
announce:9100 announce:9100 PUT /api/v1/leases
│ │ │
stratum1-a stratum1-b gateway:4929
PUT:9101 PUT:9101 │
writes .cvmfspublished
│
stratum0 (apache:80)
serves CAS + manifests
│
cvmfs-client (FUSE)
verifies file visibility
vmagent ──scrapes──► cvmfs-prepub, gateway, stratum1-a, stratum1-b
vmagent ──push──► victoriametrics:8428
grafana ──queries──► victoriametrics:8428
publisher ──POST /api/v1/jobs──► cvmfs-prepub:8080
│
publish announce
│
mosquitto:1883
┌─────────┤
subscribe │ │ subscribe
│ │
stratum1-a stratum1-b
PUT:9101 PUT:9101 ◄── data plane (direct HTTP)
│
gateway:4929
│
writes .cvmfspublished
The testbed uses a fixed directory convention so that no path variables need to be set in .env. Clone or symlink the source trees directly inside the cvmfs-testbed/ checkout, then run install.sh once to populate software/.
cvmfs-testbed/ ← this repository
├── Makefile # Top-level build and lifecycle automation
├── testbed.sh # Management script (called by Makefile)
├── install.sh # Populates software/ from cvmfs/build/
├── init.sh # One-time host setup (called by testbed.sh init)
├── docker-compose.yml # Core services (HTTP mode)
├── docker-compose.mqtt.yml # Overlay: MQTT control plane
├── docker-compose.bits.yml # Overlay: Gitea + seeder for bits-console
├── .env.example # Environment template (secrets only)
├── README.md # This file
│
├── cvmfs/ # ← CVMFS source tree (git clone or symlink HERE)
│ ├── cvmfs/
│ │ ├── make_cvmfs_server.sh
│ │ └── server/ # Patched server shell scripts
│ └── build/ # cmake build output (run cmake + make here)
│ ├── cvmfs_publish
│ ├── cvmfs_swissknife
│ └── libcvmfs_server.so.*
│
├── cvmfs-bits/ # ← cvmfs-bits source tree (git clone or symlink HERE)
│ │ # Builds cvmfs-prepub and cvmfs_gateway binaries
│ └── Makefile # `make build` produces the binaries
│
├── bits-console/ # ← bits-console source (git clone or symlink HERE)
│ │ # Required only for the --bits overlay
│ └── build-communities.sh
│
├── software/ # ← Populated by install.sh (never edit manually)
│ ├── cvmfs_server # Rebuilt from patched cvmfs/cvmfs/server/
│ ├── cvmfs_publish
│ ├── cvmfs_swissknife
│ ├── libcvmfs_server.so.*
│ ├── cvmfs-prepub # Built by cvmfs-bits/Makefile, installed by install.sh
│ ├── cvmfs_gateway # Built by cvmfs-bits/Makefile, installed by install.sh
│ ├── cvmfs2 # ← copy your built binary here manually
│ └── cvmfs_talk # ← copy your built binary here manually
│
├── tools/
│ └── dump-catalogs.sh # Decompress and SQL-dump all catalogs (host-side)
│
├── gateway/
│ └── Dockerfile
├── stratum0/
│ ├── Dockerfile
│ └── httpd.conf
├── cvmfs-prepub/
│ └── Dockerfile
├── stratum1/
│ └── Dockerfile
├── publisher/
│ ├── Dockerfile
│ └── scripts/
│ ├── smoke-test.sh # Comprehensive smoke test (bits path)
│ ├── stress-test.sh # Stress test (bits path)
│ └── make-test-payload.sh # Shared comprehensive test payload builder
├── cvmfs-native-publisher/
│ ├── Dockerfile
│ ├── entrypoint.sh
│ └── scripts/
│ ├── native-smoke.sh # Comprehensive smoke test (cvmfs_server ingest)
│ ├── native-stress.sh # Stress test (ingest path)
│ └── make-test-payload.sh # Shared comprehensive test payload builder
├── cvmfs-client/
│ ├── Dockerfile
│ ├── entrypoint.sh
│ └── scripts/
│ └── verify-publish.sh
├── mosquitto/
│ └── mosquitto.conf
├── seeder/
│ ├── Dockerfile
│ └── seed.py
├── act_runner/
│ ├── config.yaml
│ └── act_runner.service
└── monitoring/
├── scrape.yml
└── grafana/
├── provisioning/
│ ├── datasources/
│ │ └── victoriametrics.yaml
│ └── dashboards/
│ └── dashboards.yaml
└── dashboards/
└── cvmfs-prepub.json
${TESTBED_ROOT}/ ← All persistent runtime state (default: $HOME/cvmfs-testbed)
├── repos/ # CVMFS repository data (created by cvmfs_server mkfs,
│ └── ${REPO_NAME}/ # symlinked from /srv/cvmfs — NOT the source tree)
│ ├── .cvmfspublished
│ ├── .cvmfswhitelist
│ └── data/
├── data/
│ ├── spool/ # cvmfs-prepub WAL journal and job state
│ ├── s1a/ # Stratum 1 receiver A CAS
│ ├── s1b/ # Stratum 1 receiver B CAS
│ ├── cvmfs-client/ # CVMFS client disk cache
│ ├── mosquitto/ # Mosquitto persistence (MQTT mode)
│ ├── mosquitto-log/ # Mosquitto logs (MQTT mode)
│ ├── gitea/ # Gitea database + repos (bits overlay)
│ └── monitoring/
│ ├── vm/ # VictoriaMetrics data
│ ├── vmagent/ # vmagent data
│ └── grafana/ # Grafana data
└── config/ # Generated configs (init.sh writes these)
├── gateway/
│ ├── gw.json
│ ├── user.json
│ └── repo.json
├── keys/ # CVMFS signing keys
├── cvmfs-prepub/
│ └── config.yaml
├── stratum1-a/
│ └── config.yaml
└── stratum1-b/
└── config.yaml
- Docker ≥ 24 with Compose v2 plugin — check:
docker compose version - openssl (secret generation) — usually pre-installed
- FUSE kernel module loaded — check:
lsmod | grep fuse; if absent:sudo modprobe fuse - CVMFS source tree cloned (or symlinked) at
cvmfs-testbed/cvmfs/git clone https://github.com/cvmfs/cvmfs cvmfscmake -S cvmfs -B cvmfs/build && make -C cvmfs/build -j$(nproc)./install.sh← copies binaries tosoftware/and rebuildscvmfs_server
- Compiled binaries for cvmfs-prepub, cvmfs_gateway, cvmfs2, cvmfs_talk copied to
software/
- bits-console source tree cloned (or symlinked) at
cvmfs-testbed/bits-console/git clone https://github.com/your-org/bits-console bits-console- No
.envchange needed — the path is fixed by convention.
- act_runner binary on the host — download from https://gitea.com/gitea/act_runner/releases
- bits build toolchain installed on the host
- The operator's Unix user must be in the docker group (runner spawns Docker containers)
- Modern browser for the SPA (Chrome, Firefox, Edge). Safari requires adding
127.0.0.1 testbed.localhostto/etc/hosts.
testbed.sh is the recommended entry point for all testbed operations. It wraps init.sh and docker compose to give a single, consistent interface.
./testbed.sh <command> [options] [args]
| Command | Description |
|---|---|
init |
Create directories, generate secrets, write configs, run mkfs |
start |
Build images (if needed) and start all services |
stop |
Stop containers (preserves state) |
restart |
Stop then start |
status |
Show container status and spot-check service health |
info |
Print all service endpoints, ports, and credentials |
logs [svc] |
Tail logs (all services or one named service) |
test |
Run comprehensive smoke test (default method: bits) |
stresstest <n> |
Stress test with n jobs (default method: bits) |
catdump [label] |
Decompress and SQL-dump all catalogs from the current snapshot |
catdiff [a] [b] |
Diff two catalog dump sets (default: ingest vs bits) |
verify <id> [path] |
Verify end-to-end file visibility for a job UUID |
clean |
Stop containers and remove all persistent host state |
reset |
clean + init + start |
Flags:
| Flag | Effect |
|---|---|
--bits |
Include the bits-console overlay (requires bits-console/ to exist) |
--mqtt |
Include the MQTT control-plane overlay |
--method bits|ingest |
Publishing path for test/stresstest: bits (REST API, default) or ingest (cvmfs_server ingest) |
-y, --yes |
Skip interactive confirmation prompts (used by make clean) |
--software-root PATH |
Override the software/ directory |
--testbed-root PATH |
Override the testbed data root (default: $HOME/cvmfs-testbed) |
Examples:
# Core stack only
./testbed.sh init
./testbed.sh start
./testbed.sh test # bits smoke test
./testbed.sh test --method ingest # native cvmfs_server ingest smoke test
./testbed.sh stresstest 20 # 20 jobs via bits REST API
./testbed.sh stresstest 20 --method ingest
./testbed.sh logs cvmfs-prepub
# Catalog structure comparison between publishing paths
./testbed.sh test --method ingest && ./testbed.sh catdump ingest
./testbed.sh test --method bits && ./testbed.sh catdump bits
./testbed.sh catdiff ingest bits # writes ingest_vs_bits.diff
# Core stack + MQTT control plane
./testbed.sh start --mqtt
# Full stack with bits-console (bits-console/ must already be present)
./testbed.sh init --bits
./testbed.sh start --bits
# Tear down everything and start fresh
./testbed.sh reset --bitsThe Makefile is the recommended entry point for day-to-day development. It encodes the full dependency chain — pull source, build, install binaries, redeploy — so a single command keeps everything in sync.
| Target | What it does |
|---|---|
make |
Pull + build cvmfs-bits, install binaries, stop → clean → init → start testbed |
make build |
git pull + make build inside cvmfs-bits/ only |
make install |
build, then copy binaries to software/ |
make redeploy |
install + stop → clean → init → start (skip the source pull) |
make clean |
Stop containers and wipe all testbed state (no confirmation prompt) |
make test |
Smoke test — bits REST API path |
make test-ingest |
Smoke test — cvmfs_server ingest path |
make test-bits |
Smoke test — bits REST API path (explicit alias) |
make stresstest |
Stress test, N jobs via bits (default N=10) |
make stresstest-ingest |
Stress test, N jobs via ingest |
make catdump-ingest |
Dump catalogs from the current snapshot, labelled ingest |
make catdump-bits |
Dump catalogs from the current snapshot, labelled bits |
make catdiff |
Diff ingest vs bits catalog dumps |
make help |
Print the target summary with current variable values |
| Variable | Default | Description |
|---|---|---|
BITS_DIR |
$(CURDIR)/cvmfs-bits |
Path to the cvmfs-bits source tree |
N |
10 |
Number of jobs for stresstest targets |
# First run — full setup from source
make
# Day-to-day iteration after changing cvmfs-bits source
make
# Rebuild and redeploy without pulling (e.g. local uncommitted changes)
make redeploy
# Run both smoke tests and compare catalog structure
make test-ingest catdump-ingest test-bits catdump-bits catdiff
# Stress test with a custom job count
make stresstest N=50
make stresstest-ingest N=50
# Use a cvmfs-bits clone that lives outside the testbed directory
make BITS_DIR=/home/user/src/cvmfs-bits
# Wipe everything and start over (no confirmation prompt)
make clean && makemake build → cd cvmfs-bits && git pull && make build
make install → make build + ./install.sh
make redeploy → make install
./testbed.sh stop
./testbed.sh clean --yes
./testbed.sh init
./testbed.sh start
make clean → ./testbed.sh stop
./testbed.sh clean --yes
make test → ./testbed.sh test --method bits
make test-ingest → ./testbed.sh test --method ingest
make stresstest → ./testbed.sh stresstest $(N) --method bits
make catdiff → ./testbed.sh catdiff ingest bits
The recommended entry point is make. It handles the full build-install-deploy sequence automatically. Use testbed.sh directly when you need finer control over individual steps.
-
Clone the repository and enter it
git clone https://github.com/your-org/cvmfs-testbed cd cvmfs-testbed -
Clone CVMFS and cvmfs-bits source trees
git clone https://github.com/cvmfs/cvmfs cvmfs cmake -S cvmfs -B cvmfs/build && make -C cvmfs/build -j$(nproc) git clone https://github.com/your-org/cvmfs-bits cvmfs-bits
-
Copy binaries that are not built by cvmfs-bits
cp /path/to/cvmfs2 software/ cp /path/to/cvmfs_talk software/ chmod +x software/* -
Full setup (builds cvmfs-bits, installs binaries, inits and starts the testbed)
sudo make
-
Run smoke tests
make test # bits REST API path make test-ingest # cvmfs_server ingest path
-
Open Grafana
http://localhost:3000 (admin / admin)
-
Clone the repository and enter it
git clone https://github.com/your-org/cvmfs-testbed cd cvmfs-testbed chmod +x testbed.sh install.sh -
Clone and build CVMFS (inside cvmfs-testbed)
git clone https://github.com/cvmfs/cvmfs cvmfs cmake -S cvmfs -B cvmfs/build make -C cvmfs/build -j$(nproc) -
Populate software/ from the CVMFS build tree
./install.sh
This copies
cvmfs_publish,cvmfs_swissknife,libcvmfs_server.so.*, and rebuilds the patchedcvmfs_serverscript — all intosoftware/. -
Copy the remaining service binaries
cp /path/to/cvmfs-prepub software/ cp /path/to/cvmfs_gateway software/ cp /path/to/cvmfs2 software/ cp /path/to/cvmfs_talk software/ chmod +x software/* -
Initialise (requires sudo for
cvmfs_server mkfs)sudo ./testbed.sh init
Creates directory structure, generates secrets, writes service configs, and initialises the CVMFS repository.
-
Start containers
# HTTP mode (default) ./testbed.sh start # MQTT control-plane mode ./testbed.sh start --mqtt
-
Run smoke test
./testbed.sh test -
Verify file visibility through the CVMFS client
./testbed.sh verify <uuid_from_smoke_test> usr/share/test/hello.txt
-
Open Grafana
http://localhost:3000 (core stack) http://localhost:3001 (when --bits overlay is active) Credentials: admin / admin
Both publishing paths use the same comprehensive test payload (built by make-test-payload.sh), which covers directory hierarchies, hard and symbolic links, a 20 MiB file to trigger chunking, unusual file names (spaces, unicode, special characters), permission modes, and empty directories.
# bits REST API path (default)
make test
# or: ./testbed.sh test --method bits
# cvmfs_server ingest path
make test-ingest
# or: ./testbed.sh test --method ingest# 10 jobs via bits REST API (default N=10)
make stresstest
# 50 jobs via ingest path
make stresstest-ingest N=50
# or: ./testbed.sh stresstest 50 --method ingestRun both paths and diff the resulting SQLite catalog dumps to inspect schema and content differences:
make test-ingest catdump-ingest test-bits catdump-bits catdiffThis writes SQL dumps to $TESTBED_ROOT/data/catalog-dumps/ingest/ and bits/, then produces ingest_vs_bits.diff. Individual testbed.sh equivalents:
./testbed.sh test --method ingest
./testbed.sh catdump ingest # dumps to data/catalog-dumps/ingest/
./testbed.sh test --method bits
./testbed.sh catdump bits # dumps to data/catalog-dumps/bits/
./testbed.sh catdiff ingest bits # writes ingest_vs_bits.diff# Source the .env to get the API token
source .env
# Create a test tar
mkdir -p /tmp/test-pkg/usr/share/test
echo "hello cvmfs" > /tmp/test-pkg/usr/share/test/hello.txt
tar -czf /tmp/test.tar.gz -C /tmp/test-pkg .
# Submit the job
curl -X POST \
-H "Authorization: Bearer ${PREPUB_API_TOKEN}" \
-F "repo=test.cvmfs.io" \
-F "path=test/manual" \
-F "tar=@/tmp/test.tar.gz" \
-F "tag_name=manual-$(date +%s)" \
http://localhost:8080/api/v1/jobs | jq .# Watch cvmfs-prepub logs
docker compose logs -f cvmfs-prepub
# Watch gateway logs
docker compose logs -f gateway
# Watch all logs
docker compose logs -fsource .env
# Get job status
curl -s -H "Authorization: Bearer ${PREPUB_API_TOKEN}" \
http://localhost:8080/api/v1/jobs/{job-id} | jq .
# Get metrics
curl -s -H "Authorization: Bearer ${PREPUB_API_TOKEN}" \
http://localhost:8080/api/v1/metricsverify-publish.sh provides a live view of how long each pipeline stage takes and confirms that published files are visible through the CVMFS FUSE client.
# Exec into the already-running cvmfs-client container
docker compose exec cvmfs-client \
verify-publish.sh <job_id> [relative/path/inside/repo]What it does:
- Fetches job metadata from
GET /api/v1/jobs/{id}to establish t₀ (job creation time). - Subscribes to
GET /api/v1/jobs/{id}/events(SSE) and records the wall-clock time each state transition arrives. - After "published", calls
cvmfs_talk -i ${REPO_NAME} remount syncto force the client to pick up the new catalog. - If an
expected_file_pathwas given, polls/cvmfs/${REPO_NAME}/${path}every 200 ms (retrying remount every 5 s) until the file appears or 60 s elapse. - Prints a timing table:
Stage | Elapsed (ms) | Delta (ms)
---------------------------+--------------+-----------
queued | 0 | 0
processing | 312 | 312
distributing | 874 | 562
leased | 941 | 67
committing | 1005 | 64
published | 1278 | 273
client remount | 1531 | 253
file visible | 2089 | 558
Exit codes: 0 = published and file visible; 1 = job failed/aborted; 2 = timeout; 3 = published but file not visible within 60 s.
The cvmfs-client container requires SYS_ADMIN capability, /dev/fuse device access, and apparmor:unconfined security option — all set in docker-compose.yml. The cvmfs2 and cvmfs_talk binaries are injected from SOFTWARE_ROOT so they can be swapped without rebuilding the image.
The client is configured with CVMFS_HTTP_PROXY=DIRECT pointing at Stratum 0, which is correct for the single-site testbed. In a multi-tier deployment you would instead point it at a Stratum 1 or Squid proxy.
By default the testbed uses direct HTTP announces between cvmfs-prepub and the Stratum 1 receivers (Option B HTTP). To test the MQTT variant instead, activate the overlay compose file:
docker compose -f docker-compose.yml -f docker-compose.mqtt.yml up -dThis adds a mosquitto container (eclipse-mosquitto:2) and passes --broker-url tcp://mosquitto:1883 to cvmfs-prepub and both Stratum 1 receivers. The data plane (CAS object PUTs on TCP 9101) is unchanged; only the announce/ready exchange moves onto MQTT.
Switch back to HTTP mode:
docker compose down
docker compose up -d # no overlay fileMosquitto logs:
docker compose logs -f mosquitto
# or inspect the log file directly:
cat ${TESTBED_ROOT}/data/mosquitto-log/mosquitto.logVerify MQTT topics with mosquitto_sub:
docker compose exec mosquitto \
mosquitto_sub -h localhost -t 'cvmfs/prepub/#' -vFor a full description of the MQTT topic schema, flow diagrams, and security controls see REFERENCE.md §20.11.
The docker-compose.bits.yml overlay adds two services to the core stack:
- gitea — a lightweight Gitea git server (gitea/gitea:1.21, ~300 MB RAM). Serves the bits-console SPA via Gitea Pages at
http://testbed.localhost:3000/bits-project/testbed/and provides the REST API and Actions CI athttp://localhost:3000/api/v1/. - seeder — a one-shot Python container that runs after Gitea becomes healthy. It creates the
testbedorganisation andbits-projectrepository, pushes bits-console source and the testbed community config to themainbranch, builds the Pages static tree, pushes it to thegh-pagesbranch, sets CI variables and secrets, and prints the act_runner registration token.
The overlay also remaps Grafana from port 3000 to port 3001 to avoid conflict with Gitea.
-
Clone (or symlink) bits-console inside cvmfs-testbed
git clone https://github.com/your-org/bits-console bits-console # or: ln -s /path/to/your/bits-console bits-consoleNo
.envchange needed — the path is fixed by convention. -
Start all services (core + Gitea + seeder)
docker compose \ -f docker-compose.yml \ -f docker-compose.bits.yml \ up -d
-
Watch the seeder output — it will print the act_runner registration token when done:
docker compose logs -f seeder
-
Register and start act_runner on the host
# Copy config template sudo mkdir -p /var/lib/act_runner /etc/act_runner sudo cp act_runner/config.yaml /etc/act_runner/config.yaml # Register (use the token printed by the seeder) act_runner register \ --instance http://localhost:3000 \ --token <TOKEN_FROM_SEEDER> \ --name bits-host-runner \ --labels self-hosted,bits,ubuntu-latest \ --no-interactive # Install as a systemd service sudo cp act_runner/act_runner.service /etc/systemd/system/ # Edit the User= line if your username differs from cvmfs-testbed sudo systemctl daemon-reload sudo systemctl enable --now act_runner sudo systemctl status act_runner
-
Open bits-console in a browser
http://testbed.localhost:3000/bits-project/testbed/Log in with the Gitea admin credentials (see
.env). -
Submit a build job from the bits-console UI. The workflow dispatches to Gitea Actions, act_runner picks it up, runs
bits buildandbits publishon the host, and cvmfs-prepub handles the rest. -
Monitor Grafana (moved to port 3001 when the overlay is active):
http://localhost:3001 (admin / admin)
bits-console is served from testbed.localhost:3000 (a Gitea Pages subdomain) and calls the Gitea API at localhost:3000 — two different browser origins. Gitea is configured with CORS_ALLOW_DOMAIN = http://testbed.localhost:3000 to allow this.
All modern browsers (Chrome, Firefox, Edge) resolve *.localhost to 127.0.0.1 automatically per RFC 6761 — no /etc/hosts changes needed. Safari is the exception: add this line to /etc/hosts:
127.0.0.1 testbed.localhost
The seeder injects a testbed community config at communities/testbed/ui-config.yaml in the repository. Key fields used by bits-console:
api_type: gitea # routes to Gitea API instead of GitLab
gitlab_url: http://localhost:3000
project_id: testbed/bits-project # org/repo string (not an integer)
workflow_file: bits-publish.yaml
cvmfs_repo: test.cvmfs.ioThe seeder is idempotent: running it again after resetting Gitea is safe. It will recreate all resources. To force a re-seed without restarting all services:
docker compose -f docker-compose.yml -f docker-compose.bits.yml \
up seeder --force-recreate- URL: http://localhost:3000 (core stack) or http://localhost:3001 (when bits overlay is active)
- Credentials: admin / admin
- Dashboard: "cvmfs-prepub testbed" (auto-loaded)
- Metrics: Updated every 30 seconds
The dashboard includes:
- Job Submission Rate: How many jobs are being submitted per second
- Pipeline Duration: p50, p95, p99 latencies for the full publishing workflow
- CAS Upload Duration: How long it takes to upload objects
- Stratum 1 Distribution: Time to replicate to each receiver
- Object Counts: CAS occupancy and S1 object reception rates
- Dedup Hit Rate: Percentage of objects already in CAS
- Lease Metrics: Lease acquisition time and heartbeat errors
- URL: http://localhost:8428 (internal only)
- Data Retention: 1 month by default
- Query Language: PromQL (Prometheus-compatible)
- Scrapes metrics from cvmfs-prepub (port 8080), gateway (4929), and both Stratum 1 receivers (9100)
- Pushes metrics to VictoriaMetrics every 15 seconds
- Config:
monitoring/scrape.yml
The easiest way is via testbed.sh reset:
# Core stack
sudo ./testbed.sh reset
# With bits overlay (bits-console/ must already be present)
sudo ./testbed.sh reset --bitsreset runs clean (stops containers, removes all host state) then init then start.
Manual steps if you prefer:
docker compose down -v
source .env
sudo rm -rf ${TESTBED_ROOT}/data ${TESTBED_ROOT}/cvmfs ${TESTBED_ROOT}/config
rm -f "$HOME/cvmfs-testbed/.env"
./install.sh # re-populate software/ from cvmfs/build/
sudo ./testbed.sh init # or: sudo ./init.sh
./testbed.sh startCheck logs:
docker compose logs <service-name>Common issues:
- cvmfs-prepub can't connect to gateway: gateway may not be running. Check:
docker compose exec cvmfs-prepub curl -v http://gateway:4929 - Binaries not found: Ensure SOFTWARE_ROOT paths are correct in .env and binaries are copied and executable.
- Permission denied on /data: Containers run as user
prepub. Ensure the host directories are writable by your user.
If jobs hang in "pending" state:
- Check gateway logs:
docker compose logs gateway - Verify the HMAC secret is correct:
docker compose exec cvmfs-prepub env | grep HMAC - Check Stratum 1 connectivity:
docker compose exec cvmfs-prepub curl -v http://stratum1-a:9100/metrics
- Check vmagent:
docker compose logs vmagentfor scrape errors - Verify scrape config:
docker compose exec vmagent cat /etc/vmagent/scrape.yml - Test VictoriaMetrics:
curl -s http://localhost:8428/api/v1/query?query=up | jq .
- Check node IDs:
docker compose logs stratum1-a | grep node_id - Verify control channel:
docker compose exec cvmfs-prepub curl -v http://stratum1-a:9100/api/v1/info - Check data upload:
docker compose logs stratum1-a | grep "PUT"
- Ensure
/dev/fuseexists on the host:ls -la /dev/fuse - Ensure the FUSE kernel module is loaded:
lsmod | grep fuse(if absent:sudo modprobe fuse) - Check AppArmor status:
aa-status | grep cvmfs— the container must run unconfined - Inspect entrypoint output:
docker compose logs cvmfs-client - Verify the public key was copied:
docker compose exec cvmfs-client ls /etc/cvmfs/keys/
- Confirm the job actually reached "published":
docker compose logs cvmfs-prepub | tail -40 - Force a manual remount:
docker compose exec cvmfs-client cvmfs_talk -i ${REPO_NAME} remount sync - Check that Stratum 0 is serving the new manifest:
docker compose exec cvmfs-client curl -sI http://stratum0/cvmfs/${REPO_NAME}/.cvmfspublished
- Check the client cache is not stale:
docker compose exec cvmfs-client cvmfs_talk -i ${REPO_NAME} cache list
- Check seeder logs:
docker compose logs seeder - Verify
BITS_CONSOLE_SRCis set and points to a valid bits-console source tree - Ensure
build-communities.shis present and executable in the bits-console root - If Gitea itself failed to start:
docker compose logs gitea - Re-run the seeder:
docker compose -f docker-compose.yml -f docker-compose.bits.yml up seeder --force-recreate
- Confirm the seeder completed successfully (exited 0)
- Check the
gh-pagesbranch was pushed: browse tohttp://localhost:3000/testbed/bits-project/src/branch/gh-pages - Gitea Pages uses the subdomain format
{org}.{PAGES_DOMAIN}:{port}/{repo}/; thetestbedorg →testbed.localhost:3000/bits-project/testbed/ - Safari: add
127.0.0.1 testbed.localhostto/etc/hosts
- Check runner status:
systemctl status act_runner - Verify registration:
act_runner list(should show the runner as online in Gitea) - Ensure the runner labels match
runs-on: [self-hosted, bits]in the workflow - Check runner logs:
journalctl -u act_runner -f - Confirm the runner user is in the
dockergroup:groups $USER
- Ensure the
bitsbinary is in the runner's PATH (seeact_runner/act_runner.serviceEnvironment=PATH=...) - Check that
PREPUB_API_TOKENis set as a repository secret in Gitea - View the full step log in the Gitea Actions UI at
http://localhost:3000/testbed/bits-project/actions
- Check Mosquitto is up:
docker compose logs mosquitto - Verify broker is reachable:
docker compose exec cvmfs-prepub curl -v telnet://mosquitto:1883(should connect) - Confirm
--broker-urlflag appears in the command:docker compose exec cvmfs-prepub ps aux - Subscribe to all CVMFS topics to watch live traffic:
docker compose exec mosquitto mosquitto_sub -h localhost -t 'cvmfs/prepub/#' -v
dev: true is enabled in all service configs. This means:
- TLS is disabled: Certificates are not verified or required
- Private IPs allowed: Services can announce Stratum 1 endpoints on internal docker networks
- HMAC verification is relaxed: Useful for testing but never use in production
Never use these configurations in production.
The gateway and cvmfs-prepub both have write access to ${TESTBED_ROOT}/cvmfs/${REPO_NAME}. Do not write to this directory from the host while containers are running, as you may corrupt the repository.
To test different binary versions without rebuilding images:
- Stop the relevant container:
docker compose stop cvmfs-prepub - Replace the binary:
cp /path/to/new/cvmfs-prepub ${TESTBED_ROOT}/software/ - Restart:
docker compose start cvmfs-prepub
The testbed is configured with:
- Two Stratum 1 receivers (stratum1-a and stratum1-b)
- Quorum: 0.5 (at least one receiver must acknowledge)
- commit_anyway: true (proceed even if distribution fails)
This means a job succeeds if at least one receiver receives the objects, and catalog commits to the gateway regardless.
To test with more receivers, add more services to docker-compose.yml following the stratum1-a and stratum1-b pattern. Update the stratum1_endpoints list in the cvmfs-prepub config, and adjust the quorum threshold as needed.