bootload
⚓ portal

📖 DOCS

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:

  1. Once: deploy the service and mint a scoped CI token.
  2. Every push: build an image → push it to a registry → bootload restart <service> --image <ref>.
  3. 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:

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 — recommended. For images you build, this is the fastest and most dependable source: pulls happen inside the datacenter (sub-millisecond, not an internet round-trip), they are never rate-limited the way anonymous Docker Hub pulls are, and no pull credentials are needed — your instances get a short-lived lease automatically. Storage is €0.03/GB-month.

bootload image login --org my-org --project my-project
bootload image push web:$TAG --org my-org --project my-project   # → registry.bootload.io/my-org/my-project/web:$TAG
bootload restart web --project my-project --image registry.bootload.io/my-org/my-project/web:$TAG

Your own registry (GHCR, Docker Hub, ECR, …) — use this when the image already lives there for other consumers, or your organisation requires it. Build and push with its normal tooling, then point bootload at the reference; expect internet-latency pulls, and Docker Hub rate limits without credentials. 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. Deploy hooks: CI without an API key

An API key in CI can do everything your account can. A deploy hook is a signed URL that can do exactly one thing: roll one service onto another tag of the image it already runs. Leaking it is a bad day, not a catastrophe.

Create one per service:

bootload deploy-hook create web --name "github actions"
# prints the URL, and the secret once

Store both as CI secrets, then call it after your push:

curl -fsS -X POST "$BOOTLOAD_DEPLOY_HOOK?token=$BOOTLOAD_DEPLOY_SECRET&wait=1" \
  -H 'content-type: application/json' -d '{"tag":"'"$GITHUB_SHA"'"}'

Send {"tag":"v2"} for a new tag, {"image":"<same repo>:v2"} for a full reference, or no body at all to re-pull the configured image, which is what you want for a mutable tag like :latest. An image outside the service's own repository is refused with a 400. Unknown JSON fields are ignored, so you can post a CI provider's own event payload as-is.

Signing is stronger than ?token= and works the same as wake webhooks:

X-Bootload-Signature: t=<unix>,v1=<hex hmac-sha256(secret, t + "." + body)>

?wait=1 returns the deployment that was created; without it you get a 202. One call per 20 seconds per hook. Rotate with bootload deploy-hook rotate, revoke by deleting it. The catch: a hook holds no read scope, so CI cannot poll for health with it. Use an API key when the job must wait for the rollout.

6. GitHub Actions

The shortest version, with a deploy hook and no API key at all:

- uses: bootloadio/cicd/deploy@v1
  with:
    hook-url: ${{ secrets.BOOTLOAD_DEPLOY_HOOK }}
    hook-secret: ${{ secrets.BOOTLOAD_DEPLOY_SECRET }}
    tag: ${{ github.sha }}

With an API key, so the job can wait for the rollout to go healthy and roll back if it does not:

- uses: bootloadio/cicd/setup-bootload@v1
  with:
    token: ${{ secrets.BOOTLOAD_TOKEN }}
    project: production
- uses: bootloadio/cicd/deploy@v1
  with:
    service: web
    image: registry.bootload.io/acme/production/web:${{ github.sha }}

Both actions are thin wrappers around the CLI, kept in the platform repo so they cannot drift from it. The full workflow below does the same thing without them, which is worth reading once even if you use the 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.

7. 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" --project "$PROJECT"
    - bootload image push web:$TAG --org "$ORG" --project "$PROJECT"
    - REF=registry.bootload.io/$ORG/$PROJECT/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

8. 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:

Skip this and a broken image silently never takes over while CI reports green.

Gotchas

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).