Use this workflow when CI must promote the same artifact it tested, not a
second rebuild or a mutable :latest tag. The downloadable sample lives at
/examples/ci-image-deploy/
(Node 22+, no npm dependencies) and in the
examples catalog. Source pin: Bex 0eb2036e2.
Repository-backed services are a different path: Bex rebuilds from Git on each
trigger and rejects imageUrl. See GitHub integration
and How deploys work. This guide is only for an
existing image-backed service.
Prerequisites
Confirm these against your installation before copying the workflow:
| Requirement | Where |
|---|---|
Image-backed service (runtime: image) already created | Docker deploys |
| Registry pull credential attached for private images | Docker deploys |
Machine API key (id + secret) with workspace developer role | REST & GraphQL API |
Registry push access for CI (for example GHCR via GITHUB_TOKEN) | GitHub package settings |
| Billing / payment readiness when the platform requires it | Billing setup |
Push permission and Bex pull credentials are separate. CI can push while the service still fails to pull if the workspace registry credential is missing.
Download the sample
curl -fsSLO https://bex.co/examples/ci-image-deploy-latest.zip
curl -fsSLO https://bex.co/examples/ci-image-deploy-latest.zip.sha256
shasum -a 256 -c ci-image-deploy-latest.zip.sha256
unzip ci-image-deploy-latest.zip
cd ci-image-deployConfigure GitHub
- Repository variable
BEX_SERVICE_ID— the image-backed service id. - Optional variables
BEX_API_ORIGIN/BEX_OAUTH_TOKEN_URLfor self-hosted. - Secrets
BEX_KEY_IDandBEX_KEY_SECRETfrom the dashboard API Keys page. - Actions permission to write packages when using GHCR.
The workflow concurrency group is scoped to
bex-image-deploy-<repository>-<BEX_SERVICE_ID> with
cancel-in-progress: false. Overlapping pushes for that service queue in
GitHub Actions. That does not disable dashboard deploys, deploy hooks, or
other pipelines. Canceling a GitHub job does not cancel or roll back a Bex
deployment that already started.
The sample builds linux/amd64 only. Do not publish a multi-platform manifest
unless every listed platform is tested.
What the workflow does
- Build once into a local tag (
ci-candidate:local). - Test that image (
docker run … node --test). A failing test stops the job; there is noalways()deploy step. - Tag and push the same local image, then resolve its registry
@sha256:…digest (no second application rebuild). - Run
deploy.mjswithBEX_IMAGE_URL=<name>@sha256:….
deploy.mjs exchanges the machine key for a bearer token, posts
{ "imageUrl": "…" } to POST /v1/services/{id}/deploys, records the returned
deployment id, and polls only that id until status live or a terminal
failure. Mutable tag-only refs are rejected before any POST.
Offline verification
BEX_CI_FIXTURE=1 \
BEX_IMAGE_URL='ghcr.io/example/app@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
node deploy.mjs
node scripts/handoff-harness.mjsThese fixtures exercise token and deploy request shapes against a loopback stack. They do not push images or create hosted deployments.
Recovery
| Symptom | Next step |
|---|---|
| Image tests fail | Fix the app; deploy is not invoked |
image_url_requires_sha256_digest | Pass name@sha256:<64-hex>, not :latest |
token_exchange_failed / deploy_post_auth_failed | Renew key; confirm developer role and API origin (API) |
deploy_post_failed:400 mentioning repo-backed | Service must be image-backed; use commit deploy for Git services |
Registry pull / update_failed | Check service registry credential and logs |
| Pre-deploy failure | Inspect preDeployStatus and pre-deploy logs |
deploy_post_ambiguous | Do not blind-retry POST; list deploys for a matching image, then decide |
| Deadline exceeded | Raise BEX_CI_DEADLINE_MS only after checking deploy status |
| Need prior revision | Rollbacks — not automatic on CI cancel |
Related
- Docker deploys
- How deploys work
- GitHub integration
- REST & GraphQL API
- User-authorized OAuth (browser consent; different from machine keys)