CI/CD pipelines
Wire up automated deploys so every push to your main branch builds a fresh image and rolls it onto your bootload service — with a health check that keeps the old version serving if the new one is broken.
A pipeline on bootload is three moves:
- Once: deploy the service and mint a scoped CI token.
- Every push: build an image → push it to a registry →
bootload restart <service> --image <ref>. - Guardrail: the roll is health-gated. If the new image never passes its
health check, the old replicas keep serving — and you
bootload rollback.
restart --image is the whole trick. It starts a new deployment that rolls out
one replica at a time; the public route only switches to the new image once a
replica is healthy. A bad build doesn't take your site down, it just fails to
take over.
1. Deploy the service once
CI rolls an existing service — it never creates one. Create it once so its name, URL, and size are fixed:
bootload deploy --project my-project --name web \
--image ghcr.io/acme/web:bootstrap --port 8080:http
--port must match what your image actually listens on (nginx → 80:http, a
typical Node app → 3000:http). A public HTTPS URL is assigned automatically and
printed as a route line.
2. Mint a scoped CI token
Give CI its own key — never your personal login. These are the scopes a build-and-roll pipeline needs:
bootload token create ci-web \
--scope projects:read \
--scope services:read --scope services:write \
--scope deployments:read --scope deployments:write \
--scope registry:push --scope registry:pull \
--project <project-id> \
--resource service:<service-id>
The secret (blt_…) is printed once — store it as a CI secret named
BOOTLOAD_TOKEN. A few things worth knowing:
projects:readis required even though you're only touching a service — the CLI resolves the project first. Without it, every command fails withtoken lacks scope projects:read.--projectand--resource service:<id>shrink the blast radius: a leaked key can only touch what you named.registry:push/registry:pullare only needed if you use bootload's built-in registry (below). Drop them if you build and host images elsewhere.- Revoke it any time with
bootload token revoke <id>.
3. Keep a wallet balance
Deploys and rolls draw from the project's prepaid wallet. An empty wallet fails
the roll with wallet balance is zero. Keep a balance, or turn on auto-top-up
in the portal, before you rely on CI.
4. Choose a registry
bootload's built-in registry — self-contained, no external accounts:
bootload image login --org my-org
bootload image push web:$TAG --org my-org # → registry.bootload.io/my-org/web:$TAG
bootload restart web --project my-project --image registry.bootload.io/my-org/web:$TAG
Your own registry (GHCR, Docker Hub, ECR, …) — build and push with its normal tooling, then point bootload at the reference. For a private image, store pull credentials once so bootload can pull it at deploy time:
bootload registry add ghcr --project my-project \
--url ghcr.io --username <user> --token <read:packages token>
Public images need no credentials.
5. GitHub Actions
Build and push to GHCR with the built-in GITHUB_TOKEN, then roll bootload and
wait for the rollout to go healthy — rolling back if it doesn't. GitHub's
ubuntu-latest runner uses a plaintext Docker config, so registry logins just
work.
# .github/workflows/deploy.yml
name: deploy
on:
push:
branches: [main]
concurrency: # never let two rolls race
group: deploy-web
cancel-in-progress: false
jobs:
ship:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/${{ github.repository }}/web
BOOTLOAD_TOKEN: ${{ secrets.BOOTLOAD_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Build and push
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
TAG=${{ github.sha }}
docker build -t "$IMAGE:$TAG" .
docker push "$IMAGE:$TAG"
echo "REF=$IMAGE:$TAG" >> "$GITHUB_ENV"
- name: Install the bootload CLI
run: curl -fsSL https://bootload.io/v1/cli/install.sh | sh
- name: Roll and verify
env:
PROJECT: acme-prod
SERVICE: web
run: |
bootload restart "$SERVICE" --project "$PROJECT" --image "$REF"
for i in $(seq 1 60); do
line=$(bootload deployments "$SERVICE" --project "$PROJECT" --plain | head -1)
case "$line" in
*succeeded*) echo "rolled out: $line"; exit 0 ;;
*failed*|*error*) echo "failed: $line"
bootload rollback "$SERVICE" --project "$PROJECT"; exit 1 ;;
esac
echo "… $line"; sleep 5
done
bootload rollback "$SERVICE" --project "$PROJECT"; exit 1
For a private repo image, run bootload registry add ghcr … once (step 4) so
bootload can pull it — the workflow itself doesn't change.
6. GitLab CI
This one uses bootload's built-in registry, so there's no second registry to manage:
# .gitlab-ci.yml
stages: [deploy]
deploy:
stage: deploy
image: docker:27
services: [docker:27-dind]
variables:
DOCKER_TLS_CERTDIR: "/certs"
ORG: acme
PROJECT: acme-prod
SERVICE: web
rules:
- if: $CI_COMMIT_BRANCH == "main"
resource_group: deploy-web
script:
- apk add --no-cache curl
- curl -fsSL https://bootload.io/v1/cli/install.sh | sh
- TAG=$CI_COMMIT_SHORT_SHA
- docker build -t web:$TAG .
- bootload image login --org "$ORG"
- bootload image push web:$TAG --org "$ORG"
- REF=registry.bootload.io/$ORG/web:$TAG
- bootload restart "$SERVICE" --project "$PROJECT" --image "$REF"
- |
for i in $(seq 1 60); do
line=$(bootload deployments "$SERVICE" --project "$PROJECT" --plain | head -1)
case "$line" in
*succeeded*) exit 0 ;;
*failed*|*error*) bootload rollback "$SERVICE" --project "$PROJECT"; exit 1 ;;
esac
sleep 5
done
bootload rollback "$SERVICE" --project "$PROJECT"; exit 1
7. Verifying a rollout
bootload restart --image returns as soon as the deployment is accepted, not
when it's live — so a real pipeline must wait for the result:
bootload deployments <service> --plainlists deployments newest-first, one per line:<date> <id> <status> <image>.- The roll you just started is the top row. Poll it until
succeeded(pass) orfailed/error(fail), as the examples do. - States you'll see:
pending→rolling_out→succeeded, orfailed. - On failure,
bootload rollback <service>re-deploys the previous succeeded image;bootload rollback <service> --to <deployment-id>targets a specific one.
Skip this and a broken image silently never takes over while CI reports green.
Gotchas
- Always pass
--project(and--orgwhere a command takes it) in CI — there's no interactive "current project" to fall back on. - Use
--plainfor stable, parseable output. It's automatic when output isn't a terminal, so CI gets it for free. - If a custom runner has a Docker credential helper configured (
pass,osxkeychain) that isn't set up,bootload image loginfails witherror storing credentials. PointDOCKER_CONFIGat a directory whoseconfig.jsonhas nocredsStore. GitHub and GitLab default runners are fine. - Serialize rolls with
concurrency(GitHub) orresource_group(GitLab) so two pushes don't fight over the same service. - You can't delete an image a service still deploys (
HTTP 409) — roll to another tag first.
For AI coding agents
Both flows above ship as an installable agent skill, so a coding agent can set up your pipeline for you: bootload.io/skills/ci-cd-on-bootload/SKILL.md (and deploy-on-bootload for a first manual deploy).