Eval sandbox deployment (Coolify)
The eval sandbox is a minimal vox mcp HTTP gateway that exposes /v1/eval for the static docs playground. It does not mount a workspace or provider keys. Production URL: https://eval.voxlang.org. The documentation site (https://voxlang.org) is separate (GitHub Pages / mdBook), not this stack.
Compose and container image
Section titled “Compose and container image”- SSOT compose:
vox-eval.compose.ymlat the repository root (docker/vox-eval.compose.ymlis a mirror for compose invocations rooted indocker/). - Image:
ghcr.io/brbrainerd/vox-eval:latest, built by.github/workflows/docker-eval.ymlwithVOX_CLI_FEATURES=mcp-server(matchescommand: ["vox", "mcp"]). Published under the personalbrbrainerdnamespace rather thanvox-foundation: the org’s free-tier plan blocks changing container-package visibility, and this image needs to be anonymously pullable (see below) since it carries no secrets/workspace by design. The push step authenticates with aGHCR_PATrepo secret (a personal-account PAT withwrite:packages), notGITHUB_TOKEN—GITHUB_TOKENcan only publish to packages under the repo’s own owner. - Health: Docker
HEALTHCHECKand Composehealthcheckprobehttp://localhost:3921/healthviacurl(the runtime image installscurl). This aligns Traefik with an actually reachable backend. vox doctor --probe: Can be used interactively for diagnostics; it is not the canonical signal for Docker/Coolify scheduling because it exercises a broader CLI surface than the gateway-onlymcp-serverimage. PreferGET /healthfor orchestration probes.
DNS (outside Coolify)
Section titled “DNS (outside Coolify)”Point eval.voxlang.org to the same edge IP (or CNAME) where Coolify’s Traefik terminates TLS for your VPS. Coolify cannot create this record; configure it at your DNS provider (for example Cloudflare or your registrar). Confirm with dig / nslookup before debugging TLS.
Coolify configuration
Section titled “Coolify configuration”- Create or select a Docker Compose application in Coolify.
- Set FQDN / domains to
https://eval.voxlang.org(or equivalent field for your Coolify version). - Inject
VOX_MCP_HTTP_BEARER_TOKEN(and any other env vars from compose) via Coolify project/environment secrets.VOX_MCP_HTTP_READ_BEARER_TOKEN: required since the gateway bind policy (capability-profiles plan 04): a0.0.0.0bind refuses to start without both tokens. Any random value (e.g.openssl rand -hex 32); not shared with the docs site. Compose also setsVOX_MCP_HTTP_BEHIND_TUNNEL=1because Traefik terminates TLS.
ghcr.io/brbrainerd/vox-evalmust be public for Coolify’spull_policy: alwaysto fetch it without registry credentials (GitHub →brbrainerdaccount → Packages →vox-eval→ Package settings → Danger Zone → confirm visibility is Public — already done as of 2026-07-18). GHCR packages are private by default on push regardless of repo visibility, so this needs a one-time check after the package is first created — this has bitten production deploys withError response from daemon: error from registry: unauthorized/exit status 1on theeval Pullingstep even though the image built and pushed successfully moments earlier. Why a personal namespace, notvox-foundation/vox-eval: the org’s free-tier plan blocks changing an org-owned container package’s visibility entirely — that package cannot be flipped public no matter who administers the org. A personal-account package has no such restriction. Visibility is a package-level setting (not per-tag), so this is a one-time fix, not something to redo per release. (If the visibility toggle on a package’s settings page appears locked to org governance even under a personal account, check whether it’s linked to an org repo via theorg.opencontainers.image.sourcelabel —docker-eval.ymldeliberately omits that label to avoid this.)- Align Traefik / TLS with Coolify’s documented certificate workflow. Raw Compose labels use
tls.certresolver=letsencrypt; your Coolify install may prefer UI-managed certs—avoid duplicate conflicting resolvers.
API discovery and provisioning (no SSH)
Section titled “API discovery and provisioning (no SSH)”Secrets resolve through vox-secrets from canonical env vars (deploy-contract.md):
| Purpose | Env vars |
|---|---|
| Coolify base URL | COOLIFY_BASE_URL |
| Write / deploy | COOLIFY_TOKEN |
Read-only GET (optional) | COOLIFY_READ_TOKEN (falls back to COOLIFY_TOKEN) |
| Target app | COOLIFY_APP_UUID |
Discover (list apps and optional version probe):
vox ci coolify-eval discoverSync compose from the repo (updates docker_compose_raw, optional domains, then GET /api/v1/deploy?uuid= unless --no-deploy):
vox ci coolify-eval sync-compose \ --compose vox-eval.compose.yml \ --domains https://eval.voxlang.orgVerify COOLIFY_APP_UUID matches the eval compose application (vox ci coolify-eval discover prints uuid and fqdn). deploy-hetzner.yml Gate 3 assumes this UUID backs https://eval.voxlang.org/health unless COOLIFY_PUBLIC_EVAL_HEALTH_URL overrides it. Gate 1 is only cargo build -p vox-cli --locked on ubuntu-latest; merge-quality gates live in .github/workflows/ci.yml.
GitHub Actions
Section titled “GitHub Actions”Manual .github/workflows/coolify-eval-sync.yml (workflow_dispatch) runs discover and optionally sync-compose using repository secrets. Exact Coolify PATCH shapes may differ by major version; treat failures as a signal to confirm API docs for your install.
deploy-hetzner.yml Gate 3: The public curl probe must still run when Gate 1 is skipped (workflow_dispatch + skip_tests: true). The health-check job should needs: [smoke-ci, deploy-coolify] with if: ${{ always() && needs.deploy-coolify.result == 'success' }} so GitHub Actions does not skip the HTTPS verifier just because smoke CI did not run.
Recovery loop (everything except Cloudflare)
Section titled “Recovery loop (everything except Cloudflare)”Use this repeat-until-green procedure when curl -fsS https://eval.voxlang.org/health fails (TLS errors, 503 no available server, etc.). Symptom → cause mapping: deploy-contract Gate 3 cheatsheet.
Step 1 — Identity (COOLIFY_APP_UUID vs eval app)
Section titled “Step 1 — Identity (COOLIFY_APP_UUID vs eval app)”Prerequisites: COOLIFY_BASE_URL and a read-capable bearer (COOLIFY_READ_TOKEN or COOLIFY_TOKEN) in the environment, or resolve the same values via vox-secrets locally.
vox ci coolify-eval discoverConfirm exactly one application is the eval stack: docker_compose_raw is present (compose app), fqdn / name matches eval.voxlang.org. Align GitHub repository secret COOLIFY_APP_UUID with that row’s uuid so deploy-hetzner.yml Gate 3 targets the correct resource.
Step 2 — Compose sync and secrets (Coolify + GHCR)
Section titled “Step 2 — Compose sync and secrets (Coolify + GHCR)”Push SSOT compose from this repo (writes Coolify application state; requires write/deploy token):
vox ci coolify-eval sync-compose \ --compose vox-eval.compose.yml \ --domains https://eval.voxlang.orgOr run .github/workflows/coolify-eval-sync.yml with sync_compose: true.
In Coolify, set VOX_MCP_HTTP_BEARER_TOKEN, VOX_MCP_HTTP_READ_BEARER_TOKEN, and any other env vars referenced by vox-eval.compose.yml. ghcr.io/brbrainerd/vox-eval must be public (see above) — Coolify has no registry credentials configured for it. Wait until the deployment finishes and the service is running and healthy (Compose healthcheck uses curl → http://localhost:3921/health).
Step 3 — TLS and Traefik router
Section titled “Step 3 — TLS and Traefik router”Choose one TLS strategy: Coolify UI FQDN / Generate TLS certificates for eval.voxlang.org, or Traefik tls.certresolver=letsencrypt in labels — avoid duplicate conflicting resolvers (see §Coolify configuration above). Confirm the Traefik Host rule matches eval.voxlang.org (same as vox-eval.compose.yml). For HTTP-01 ACME, port 80 must reach Traefik on the origin.
Step 4 — Backend (503)
Section titled “Step 4 — Backend (503)”If TLS verifies but you still see 503 / no available server, inspect Coolify/Docker logs for the eval container. Inside the container, GET /health on port 3921 must return 200. Typical fixes: wrong COOLIFY_APP_UUID, missing VOX_MCP_HTTP_*, failed image pull, or Host mismatch.
Step 5 — CI alignment (optional)
Section titled “Step 5 — CI alignment (optional)”After the public URL is green, push main or run Deploy Hetzner (Coolify) so .github/workflows/deploy-hetzner.yml Gate 3 passes.
Cloudflare (operator-only; not automated here)
Section titled “Cloudflare (operator-only; not automated here)”Do this at your DNS provider when eval.voxlang.org uses Cloudflare:
- DNS:
eval→A/AAAA(orCNAME) to the Coolify/VPS public target Cloudflare documents for your setup. Confirm withdig eval.voxlang.org. - Proxy: DNS only (grey) simplifies origin TLS/Let’s Encrypt while debugging. Proxied (orange) visitors see Cloudflare’s certificate; set SSL/TLS to Full or Full (strict) when the origin presents a valid cert. Avoid Flexible if you rely on
VOX_MCP_HTTP_REQUIRE_FORWARDED_HTTPS=1(see compose). - ACME: For HTTP-01, ensure
http://eval.voxlang.org/.well-known/acme-challenge/is not blocked by rules. Prefer DNS-01 in Coolify if HTTP-01 is impractical; that may require a Cloudflare API token (DNS:Edit) configured in Coolify, not in this repository. - Wrong origin: Ensure
evalis not a CNAME to GitHub Pages or another unrelated host.
Verification
Section titled “Verification”Loop exit: from a machine with a normal CA store (no -k):
curl -fsS https://eval.voxlang.org/healthExpect HTTP 200. Repeat until success after each Coolify/DNS change.
Periodic check (every ~3s, PowerShell):
while ($true) { curl.exe -fsS "https://eval.voxlang.org/health" 2>$null if ($LASTEXITCODE -eq 0) { break } Start-Sleep -Seconds 3}Periodic check (bash):
until curl -fsS https://eval.voxlang.org/health; do sleep 3; done