| Service | Profile | Image |
|---|---|---|
postgres |
default | postgres@sha256:33f9… (16) |
validator |
default | build deploy/Dockerfile target validator |
updater |
default | build target updater |
socket-proxy |
default | tecnativa/docker-socket-proxy@sha256:9e4b… |
gateway |
master |
build target gateway |
Default docker compose up -d starts 4 services and does not start gateway.
Owner host: docker compose --profile master up -d starts 5.
- No floating image tags (digest pins only).
/var/run/docker.sockonly onsocket-proxy(read-only).- socket-proxy allowlist:
CONTAINERS=1 IMAGES=1 POST=1(matchesupdater). - Secrets via age-decrypted env files mode 0600 under
deploy/env/*.env— never in images or cloud-init.
# 1) Release binaries (or set BASE_DOCKER_BUILD_FROM=source for full in-Docker rustc 1.96)
cargo build --release -p validator-bin -p gateway-bin -p updater-bin
# 2) Env files at 0600
./deploy/scripts/materialize-env.sh
# 3) Build service images + start default stack
export BASE_DOCKER_BUILD_FROM=prebuilt
docker compose build
docker compose up -d
docker compose ps
# 4) Master profile (gateway)
docker compose --profile master up -dMaster + gateway + validator + challenges on testnet 541, with an ephemeral
cloudflared public URL for the gateway. Procedure:
docs/runbooks/local-testnet-e2e.md.
./deploy/scripts/local-e2e.sh --help
./deploy/scripts/local-e2e.sh --dry-run
./deploy/scripts/local-e2e.sh --smoke # or --live when wallets are present# On operator machine
age -r "$RECIPIENT" -o deploy/env/postgres.env.age deploy/env/postgres.env
# On droplet (identity delivered out of band)
export AGE_IDENTITY=/etc/base/age-identity.txt
./deploy/scripts/materialize-env.sh| Host | Droplet | VPC IP | Role | Hotkey | Gateway |
|---|---|---|---|---|---|
| staging master | base-staging (68.183.23.51) |
10.116.0.2 | owner control plane | yes (BASE_GATEWAY_HOTKEY) |
yes (--profile master) — public API staging.api.joinbase.ai (BASE_DOMAIN, cleartext :80/:8080) |
| staging validator | base-staging-validator |
10.116.0.4 | normal validator | no | no — uses master gateway over VPC :8080 |
DNS (operator): staging.api.joinbase.ai A → staging master public IPv4 (STAGING_MASTER_HOST / 68.183.23.51).
| prod master | base-prod | 10.116.0.3 | owner control plane | yes | yes |
| prod validator | base-prod-validator | 10.116.0.5 (assigned) | normal validator | no | no — uses prod master gateway over VPC :8080 |
Deploy (manual or via CI):
export BASE_SSH_IDENTITY=~/.ssh/id_ed25519
# Staging master (testnet 541)
./deploy/scripts/remote-deploy.sh \
--host root@68.183.23.51 --role master --env staging \
--bootstrap-secrets-from root@68.183.23.51
# Staging validator (points at master VPC gateway)
./deploy/scripts/remote-deploy.sh \
--host root@142.93.197.253 --role validator --env staging \
--gateway-endpoint http://10.116.0.2:8080 \
--bootstrap-secrets-from root@68.183.23.51
# Prod master
./deploy/scripts/remote-deploy.sh \
--host root@206.189.224.155 --role master --env prod \
--bootstrap-secrets-from root@206.189.224.155
# Prod validator (points at prod master VPC gateway)
./deploy/scripts/remote-deploy.sh \
--host root@<prod-validator-ip> --role validator --env prod \
--gateway-endpoint http://10.116.0.3:8080 \
--bootstrap-secrets-from root@206.189.224.155| File | Purpose |
|---|---|
deploy/compose/role-master.yml |
gateway profile, VPC :8080 publish, loopback tunnels |
deploy/compose/role-validator.yml |
gateway disabled, VPC gateway endpoint |
deploy/compose/env-staging.yml |
testnet 541, wss://test.finney.opentensor.ai:443, 3s coordination |
deploy/compose/env-prod.yml |
mainnet, conservative intervals |
deploy/compose/env-local.yml |
local-only overlay (on top of staging); used by local-e2e.sh |
| `remote-deploy.sh --env staging | prod --role master |
combination. Verify locally: ./deploy/scripts/assert-compose-matrix.sh. |
.github/workflows/deploy-staging.yml— after successfulcionmain(and manual dispatch).github/workflows/deploy-prod.yml— on push ofv*.*.*tags frommain(and manual dispatch with SHA)
Prod release flow (tag-based):
- CI passes on
mainfor commit X. images.ymlbuilds/pushes GHCR digests for X, promotes pin services intodeploy/pins/staging.json, commits digests + pins tomain.- Staging droplets may still deploy with
--build-from source(iteration); the pin ladder is what authorizes prod. - Operator cuts
git tag vX.Y.Zon commit X and pushes the tag. deploy-prod.ymlpreflight: CI green for X;origin/mainstaging pinscommit_sha == X.- Fail-closed Postgres backup (SSH dump on prod master → DO Spaces), then
promote.sh --env prod --confirm-prodper service. - Both prod hosts:
remote-deploy.sh --build-from registry(pull GHCR@sha256, retag to Compose tags,up --no-build). - Smoke
/healthz.environment: production(enable required reviewers in GitHub UI).
Required GitHub secrets:
| Secret | Purpose |
|---|---|
STAGING_SSH_KEY |
private key for droplet SSH |
STAGING_MASTER_HOST |
public IPv4 of base-staging |
STAGING_VALIDATOR_HOST |
public IPv4 of base-staging-validator |
STAGING_MASTER_GATEWAY_URL |
optional, default http://10.116.0.2:8080 |
PROD_HOST |
public IPv4 of base-prod |
PROD_SSH_KEY |
optional override of staging key |
PROD_VALIDATOR_HOST |
public IPv4 of base-prod-validator |
PROD_MASTER_GATEWAY_URL |
optional, default http://10.116.0.3:8080 |
BASE_BACKUP_ENDPOINT |
DO Spaces endpoint (e.g. https://nyc3.digitaloceanspaces.com) — required for prod promote (fail-closed) |
SPACES_ACCESS_KEY_ID |
Spaces access key (fallback: AWS_ACCESS_KEY_ID) |
SPACES_SECRET_ACCESS_KEY |
Spaces secret (fallback: AWS_SECRET_ACCESS_KEY) |
BASE_BACKUP_BUCKET |
optional, default base-backups |
Not AWS EKS. Control plane stays Docker Compose on DigitalOcean droplets (existing design). A separate DOKS cluster on this account (
basecrawl-prod-nyc3) is unrelated and must not host base.
Terraform lives in terraform/: two s-8vcpu-16gb-amd droplets
(base-staging, base-prod) in nyc1 (nyc3 has no 8vCPU/16GB slug on this account) plus a firewall (SSH from operator IP
only; 80/443 open). Cloud-init installs Docker + Compose only.
Age delivery helpers:
# Encrypt (operator machine; recipient = age public key)
./deploy/scripts/age-encrypt-env.sh \
--recipient "$(age-keygen -y /path/to/age-identity.txt)" \
--src-dir deploy/env \
--out-dir /tmp/base-env-age
# After OOB identity install on the droplet:
./deploy/scripts/age-push-env.sh --host root@DROPLET_IP --age-dir /tmp/base-env-age --materializeSee terraform/README.md for apply steps and R11 notes.
The base-hosts firewall allows port 22 from the operator IP only. GitHub
runners get ephemeral Azure addresses that cannot be allowlisted ahead of time,
so the deploy jobs use .github/actions/do-firewall:
it adds an inbound rule for the runner's own /32, and an if: always() step
removes exactly that rule afterwards. Port 22 is closed to the world at rest.
This needs a DIGITALOCEAN_TOKEN repository secret. Two caveats:
- A runner killed between the two steps leaves its
/32behind. Audit withdoctl compute firewall get base-hostsand delete anything that is not the operator IP. terraform applyrewrites the firewall's whole rule set, so it will drop a live ephemeral rule. Do not apply Terraform while a deploy is running.
Never enable in production. Adversarial staging harness:
docker compose --profile evil-gateway config --services # must list evil-gateway
docker compose --profile master config --services # must NOT list evil-gateway
./deploy/scripts/assert-evil-gateway-not-default.shOffline proofs (no live TAO): cargo test -p validator a48_
Digest-only rollout with backup-before-pin and fail-closed prod.
# 1) CI (or local) records digests after build
./deploy/scripts/record-image-digests.sh
# 2) Promote known-good digest to staging (backs up Postgres first)
export PGHOST=... PGUSER=... PGPASSWORD=... PGDATABASE=base
export BASE_BACKUP_ENDPOINT=https://nyc3.digitaloceanspaces.com # or local MinIO
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...
export BASE_BACKUP_BUCKET=base-backups
./deploy/scripts/promote.sh \
--env staging --service validator \
--image ghcr.io/org/validator@sha256:<64-hex>
# 3) After staging is healthy, promote same digest to prod
./deploy/scripts/promote.sh \
--env prod --service validator --confirm-prod \
--image ghcr.io/org/validator@sha256:<64-hex>
# 4) Rollback = re-promote previous snapshot
./deploy/scripts/promote.sh --env staging --service validator --rollback
# 5) Restore drill (scratch DB row-count match)
./deploy/scripts/pg-restore-drill.sh --s3-uri s3://base-backups/pg/staging/<stamp>.sql.gzPin files: deploy/pins/staging.json, deploy/pins/prod.json.
Staging promote never writes the prod pin. Prod promote requires staging ladder + --confirm-prod.
Updater consumes BASE_UPDATER_DESIRED_IMAGE (also written to deploy/pins/<env>.desired.env).
Verify locally: ./deploy/scripts/verify-task-43.sh