Skip to content

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.vox-lang.org. The documentation site (https://vox-lang.org) is separate (GitHub Pages / mdBook), not this stack.

  • SSOT compose: vox-eval.compose.yml at the repository root (docker/vox-eval.compose.yml is a mirror for compose invocations rooted in docker/).
  • Image: ghcr.io/vox-foundation/vox-eval:latest, built by .github/workflows/docker-eval.yml with VOX_CLI_FEATURES=mcp-server (matches command: ["vox", "mcp"]).
  • Health: Docker HEALTHCHECK and Compose healthcheck probe http://localhost:3921/health via curl (the runtime image installs curl). 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-only mcp-server image. Prefer GET /health for orchestration probes.

Point eval.vox-lang.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.

  1. Create or select a Docker Compose application in Coolify.
  2. Set FQDN / domains to https://eval.vox-lang.org (or equivalent field for your Coolify version).
  3. Inject VOX_MCP_HTTP_BEARER_TOKEN (and any other env vars from compose) via Coolify project/environment secrets.
  4. If the GHCR package is private, configure registry credentials in Coolify so pull_policy: always can fetch ghcr.io/vox-foundation/vox-eval.
  5. 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.

Secrets resolve through vox-secrets from canonical env vars (deploy-contract.md):

PurposeEnv vars
Coolify base URLCOOLIFY_BASE_URL
Write / deployCOOLIFY_TOKEN
Read-only GET (optional)COOLIFY_READ_TOKEN (falls back to COOLIFY_TOKEN)
Target appCOOLIFY_APP_UUID

Discover (list apps and optional version probe):

Terminal window
vox ci coolify-eval discover

Sync compose from the repo (updates docker_compose_raw, optional domains, then GET /api/v1/deploy?uuid= unless --no-deploy):

Terminal window
vox ci coolify-eval sync-compose \
--compose vox-eval.compose.yml \
--domains https://eval.vox-lang.org

Verify 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.vox-lang.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.

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.vox-lang.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.

Terminal window
vox ci coolify-eval discover

Confirm exactly one application is the eval stack: docker_compose_raw is present (compose app), fqdn / name matches eval.vox-lang.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):

Terminal window
vox ci coolify-eval sync-compose \
--compose vox-eval.compose.yml \
--domains https://eval.vox-lang.org

Or run .github/workflows/coolify-eval-sync.yml with sync_compose: true.

In Coolify, set VOX_MCP_HTTP_BEARER_TOKEN and any other env vars referenced by vox-eval.compose.yml. If ghcr.io/vox-foundation/vox-eval is private, add registry credentials in Coolify. Wait until the deployment finishes and the service is running and healthy (Compose healthcheck uses curlhttp://localhost:3921/health).

Choose one TLS strategy: Coolify UI FQDN / Generate TLS certificates for eval.vox-lang.org, or Traefik tls.certresolver=letsencrypt in labels — avoid duplicate conflicting resolvers (see §Coolify configuration above). Confirm the Traefik Host rule matches eval.vox-lang.org (same as vox-eval.compose.yml). For HTTP-01 ACME, port 80 must reach Traefik on the origin.

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.

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.vox-lang.org uses Cloudflare:

  1. DNS: evalA / AAAA (or CNAME) to the Coolify/VPS public target Cloudflare documents for your setup. Confirm with dig eval.vox-lang.org.
  2. 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).
  3. ACME: For HTTP-01, ensure http://eval.vox-lang.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.
  4. Wrong origin: Ensure eval is not a CNAME to GitHub Pages or another unrelated host.

Loop exit: from a machine with a normal CA store (no -k):

Terminal window
curl -fsS https://eval.vox-lang.org/health

Expect HTTP 200. Repeat until success after each Coolify/DNS change.

Periodic check (every ~3s, PowerShell):

Terminal window
while ($true) {
curl.exe -fsS "https://eval.vox-lang.org/health" 2>$null
if ($LASTEXITCODE -eq 0) { break }
Start-Sleep -Seconds 3
}

Periodic check (bash):

Terminal window
until curl -fsS https://eval.vox-lang.org/health; do sleep 3; done